@awak-app/simy-cli 0.6.22 → 0.6.23

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 CHANGED
@@ -236,6 +236,15 @@ SQM checks apply when you explicitly select `@simy`, invoke the SQM skill, or as
236
236
  After installation, restart Codex and find and install **SIMY** in **Plugins**
237
237
  before using `@simy` in a new task.
238
238
 
239
+ If **Connect SIMY** fails while the CLI is already signed in, check the recovery
240
+ message in connection settings. Finish or retry a pending SIMY update, or complete
241
+ or cancel an organization selection. If SIMY remains paused, use **SIMY → Quit
242
+ SIMY**, reopen it, and reconnect; closing the window alone does not restart the
243
+ background host. Refresh connections and confirm the conversation list returns.
244
+ Diagnostic `provider connection failed` entries distinguish `reason=update`,
245
+ `organization_change`, and `shutdown` without including credentials or transcripts.
246
+ Reconnection does not sign in to Codex CLI or restore revoked automatic continuation.
247
+
239
248
  The plugin also includes a non-blocking connection reminder. At task startup,
240
249
  resume, and prompt submission it reads the origin-scoped local discovery record
241
250
  and checks the loopback CLI health endpoint. It warns once per disconnected
@@ -593,6 +602,15 @@ preflight and returns the installed version, required version, and a recovery
593
602
  command to SIMY Web. Update Codex with
594
603
  `npm install -g @openai/codex@latest`; update Claude Code with `claude update`.
595
604
 
605
+ Codex discovery compares installed CLI versions across PATH (including shadowed
606
+ installs) and the known Codex/ChatGPT app bundles on macOS, selecting the newest
607
+ compatible version. Equal versions retain discovery order. A compatible
608
+ `SIMY_CODEX_BIN` override retains priority; a broken or unsupported override falls
609
+ back to automatic discovery. Refresh the connection status after updating Codex;
610
+ each new inspection checks installed versions again. SIMY does not download or
611
+ replace Codex. The desktop connection status and Codex conversation reader both
612
+ require `0.158.0` or later for the verified app-server writer-lock protocol.
613
+
596
614
  ## Your Desktop executor
597
615
 
598
616
  SIMY CLI is the `desktop` execution target, shown to users as **Your Desktop**.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.6.22",
3
+ "version": "0.6.23",
4
4
  "description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,11 +32,30 @@ export async function inspectBackendExecutable(
32
32
  assertActive();
33
33
  const policy = versionPolicy(backend);
34
34
  const requiredVersion = minimumVersion || policy.minimumVersion;
35
+ const explicitCodex = backend === "codex"
36
+ ? (discoveryOptions.environment ?? process.env).SIMY_CODEX_BIN : null;
35
37
  candidates ??= [...backendCandidates(backend, discoveryOptions), ...await bundledClaudeCandidates(backend, discoveryOptions)];
38
+ candidates = [...new Set(candidates.filter(Boolean))];
36
39
  let fallback = null;
37
- for (const candidate of [...new Set(candidates.filter(Boolean))]) {
40
+ // An intentional override retains priority. Broken or unsupported overrides
41
+ // still fall back to automatic discovery, as before.
42
+ if (explicitCodex && candidates.includes(explicitCodex)) {
43
+ const pinned = await inspectBackendExecutable(backend, {
44
+ candidates: [explicitCodex], spawnImpl, timeoutMs, signal, minimumVersion,
45
+ discoveryOptions: { ...discoveryOptions, environment: {} },
46
+ });
47
+ if (pinned.compatible) return pinned;
48
+ if (pinned.available) fallback = pinned;
49
+ candidates = candidates.filter(candidate => candidate !== explicitCodex);
50
+ }
51
+ // Comparing all Codex installs must not turn several slow wrappers into a
52
+ // serial startup delay. Keep concurrency bounded and results in source order.
53
+ const probes = backend === "codex"
54
+ ? await probeCandidates(candidates, { spawnImpl, timeoutMs, signal }, assertActive) : null;
55
+ let selected = null;
56
+ for (const [index, candidate] of candidates.entries()) {
38
57
  assertActive();
39
- const probe = await probeExecutable(candidate, { spawnImpl, timeoutMs, signal });
58
+ const probe = probes ? probes[index] : await probeExecutable(candidate, { spawnImpl, timeoutMs, signal });
40
59
  assertActive();
41
60
  if (!probe.available) continue;
42
61
  const installedVersion = parseBackendVersion(probe.output);
@@ -52,12 +71,15 @@ export async function inspectBackendExecutable(
52
71
  update_command: policy.updateCommand,
53
72
  };
54
73
  // An executable that starts may still be too old for the requested feature.
55
- // Preserve candidate priority among compatible installs, and retain the
56
- // first runnable failure for actionable diagnostics if none qualifies.
57
- if (compatible) return inspection;
74
+ // Claude retains its existing priority. Codex chooses the newest version;
75
+ // equal versions retain discovery order regardless of probe completion order.
76
+ if (compatible) {
77
+ if (backend !== "codex") return inspection;
78
+ if (!selected || compareCodexVersions(installedVersion, selected.installed_version) > 0) selected = inspection;
79
+ }
58
80
  fallback ??= inspection;
59
81
  }
60
- return fallback || {
82
+ return selected || fallback || {
61
83
  backend,
62
84
  executable: null,
63
85
  available: false,
@@ -88,6 +110,8 @@ export function backendCandidates(backend, {
88
110
  return [
89
111
  environment.SIMY_CODEX_BIN,
90
112
  "codex",
113
+ ...codexPathCandidates(platform, environment),
114
+ ...(platform === "darwin" ? ["/opt/homebrew/bin/codex", "/usr/local/bin/codex"] : []),
91
115
  ...(platform === "darwin" ? ["/Applications", path.posix.join(home, "Applications")].flatMap(directory =>
92
116
  ["ChatGPT.app", "Codex.app"].flatMap(app => {
93
117
  const resources = path.posix.join(directory, app, "Contents", "Resources");
@@ -98,6 +122,48 @@ export function backendCandidates(backend, {
98
122
  ];
99
123
  }
100
124
 
125
+ function codexPathCandidates(platform, environment) {
126
+ const paths = platform === "win32" ? path.win32 : path.posix;
127
+ const names = platform === "win32" ? ["codex.exe", "codex.cmd", "codex.bat", "codex"] : ["codex"];
128
+ return (environment.PATH ?? environment.Path ?? "").split(paths.delimiter)
129
+ .map(directory => directory.replace(/^"(.*)"$/, "$1"))
130
+ .filter(directory => paths.isAbsolute(directory))
131
+ .flatMap(directory => names.map(name => paths.join(directory, name)))
132
+ .filter(candidate => { try { return statSync(candidate).isFile(); } catch { return false; } });
133
+ }
134
+
135
+ async function probeCandidates(candidates, options, assertActive) {
136
+ let next = 0;
137
+ const probes = new Array(candidates.length);
138
+ await Promise.all(Array.from({ length: Math.min(4, candidates.length) }, async () => {
139
+ while (next < candidates.length) {
140
+ // Drain every owned probe before reporting cancellation to callers.
141
+ if (options.signal?.aborted) break;
142
+ const index = next++;
143
+ probes[index] = await probeExecutable(candidates[index], options);
144
+ }
145
+ }));
146
+ assertActive();
147
+ return probes;
148
+ }
149
+
150
+ function compareCodexVersions(left, right) {
151
+ const core = compareVersions(left, right);
152
+ if (core) return core;
153
+ const prerelease = version => version.includes("-") ? version.slice(version.indexOf("-") + 1).split(".") : null;
154
+ const a = prerelease(left), b = prerelease(right);
155
+ if (!a || !b) return Number(!a) - Number(!b);
156
+ for (let index = 0; index < Math.max(a.length, b.length); index++) {
157
+ if (a[index] === b[index]) continue;
158
+ if (a[index] === undefined || b[index] === undefined) return a[index] === undefined ? -1 : 1;
159
+ const numericA = /^\d+$/.test(a[index]), numericB = /^\d+$/.test(b[index]);
160
+ if (numericA && numericB) return Number(a[index]) - Number(b[index]);
161
+ if (numericA !== numericB) return numericA ? -1 : 1;
162
+ return a[index] < b[index] ? -1 : 1;
163
+ }
164
+ return 0;
165
+ }
166
+
101
167
  // Claude Desktop can install below <version>/<content hash>/claude.app while
102
168
  // an older PATH wrapper still points at <version>/claude.app. Discover only the
103
169
  // known app layout; never recursively execute arbitrary files from user folders.
@@ -167,7 +233,9 @@ function probeExecutable(command, { spawnImpl, timeoutMs, signal }) {
167
233
  let settled = false, terminating = false;
168
234
  let timeout, force, limit;
169
235
  let output = Buffer.alloc(0);
170
- const child = spawnImpl(command, ["--version"], { stdio: ["ignore", "pipe", "pipe"] });
236
+ let child;
237
+ try { child = spawnImpl(command, ["--version"], { stdio: ["ignore", "pipe", "pipe"] }); }
238
+ catch { resolve({ available: false, output: "" }); return; }
171
239
  const capture = chunk => {
172
240
  // Version probes need a tiny prefix. Drain noisy executables without
173
241
  // retaining unbounded stdout/stderr in the desktop background process.
@@ -13,7 +13,7 @@ import {
13
13
  } from "./durable-local-task-steps-contract.js";
14
14
  import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
15
15
 
16
- export const CLI_VERSION = "0.6.22";
16
+ export const CLI_VERSION = "0.6.23";
17
17
  export const CLI_API_CONTRACT_VERSION = 10;
18
18
 
19
19
  export function withCliContract(
@@ -101,9 +101,23 @@ export async function collectDevWorkEvent({
101
101
  session_id: sha256(`${provider}:${sessionId.trim()}`),
102
102
  source,
103
103
  project_id: projectId.toLowerCase(),
104
+ ...workflowRun(env),
104
105
  };
105
106
  }
106
107
 
108
+ /**
109
+ * W-02: the independent WorkFlow run that started this session. The SIMY
110
+ * agent sets both variables only on the Provider process it launched for a
111
+ * WorkFlow run (and clears inherited values otherwise); SIMY keeps the link
112
+ * only when the run belongs to the signed-in device owner.
113
+ */
114
+ export function workflowRun(env = process.env) {
115
+ const workflowId = String(env.SIMY_WORKFLOW_ID || "").trim();
116
+ const runId = String(env.SIMY_WORKFLOW_RUN_ID || "").trim();
117
+ if (!UUID.test(workflowId) || !UUID.test(runId)) return {};
118
+ return { workflow_id: workflowId.toLowerCase(), workflow_run_id: runId.toLowerCase() };
119
+ }
120
+
107
121
  /** Best effort: the open PR for the current branch via `gh`, if installed. */
108
122
  export async function pullRequestUrl({ cwd, repo, env = process.env, runImpl = run }) {
109
123
  const url = await runImpl("gh", ["pr", "view", "--json", "url", "--jq", ".url"], {
@@ -190,7 +204,15 @@ export async function runDevWorkHook({
190
204
  env.PLUGIN_DATA?.trim() || env.CLAUDE_PLUGIN_DATA?.trim() || join(sessionRoot, "plugin-data", "simy");
191
205
  // The upstream observation is keyed by (Project, repo, HEAD, session): a
192
206
  // resend of the same state changes nothing, so skip it (and `gh`) locally.
193
- const fingerprint = sha256(JSON.stringify([event.session_id, event.repo, event.head_sha, event.project_id ?? null]));
207
+ const fingerprint = sha256(
208
+ JSON.stringify([
209
+ event.session_id,
210
+ event.repo,
211
+ event.head_sha,
212
+ event.project_id ?? null,
213
+ event.workflow_run_id ?? null,
214
+ ]),
215
+ );
194
216
  const statePath = join(pluginData, "dev-work", `${event.session_id}.json`);
195
217
  if ((await readState(statePath))?.fingerprint === fingerprint) {
196
218
  return { sent: false, reason: "unchanged" };
@@ -67,6 +67,11 @@ function desktopActionError(action, error) {
67
67
  if (error?.code === "provider_link_changed") return "SIMY connection changed. Refresh and select the conversation again.";
68
68
  if (error?.code === "provider_link_disconnected") return "Reconnect this provider to SIMY before using it.";
69
69
  if (["provider_connect", "provider_disconnect"].includes(action)) {
70
+ if (error?.code === 'desktop_stopping') {
71
+ if (error.stoppingReason === 'update') return "SIMY is preparing an update. Finish or retry the update; if it failed, quit SIMY completely, reopen it, then reconnect.";
72
+ if (error.stoppingReason === 'organization_change') return "SIMY is switching organizations. Complete or cancel the organization selection, then reconnect.";
73
+ return "SIMY is pausing new work. Quit SIMY completely, reopen it, then reconnect.";
74
+ }
70
75
  if (error?.code === 'desktop_access_required') return "Sign in to your SIMY workspace, then try connecting again.";
71
76
  if (['EACCES', 'EPERM', 'EROFS'].includes(error?.code)) return "SIMY could not save the connection settings. Check access to its local settings folder, then retry.";
72
77
  if (error?.code === 'ENOSPC') return "SIMY could not save the connection settings. Free disk space, then retry.";
@@ -301,11 +306,17 @@ export async function startDesktopHost({ input = process.stdin, output = process
301
306
  let organizationAnalysesSuspended = false;
302
307
  let organizationAnalysisDrain = Promise.resolve();
303
308
  let organizationAnalysisEpoch = 0;
309
+ const requireWorkAdmission = () => {
310
+ const stoppingReason = closed ? 'shutdown'
311
+ : stoppingForUpdate || updater?.acceptsNewWork?.() === false ? 'update'
312
+ : organizationAnalysesSuspended ? 'organization_change' : null;
313
+ if (stoppingReason) throw Object.assign(new Error("SIMY is stopping. Reopen SIMY before starting new work."), { code: "desktop_stopping", stoppingReason });
314
+ };
304
315
  const authorizeWork = async expectedScope => {
305
- if (closed || stoppingForUpdate || updater?.acceptsNewWork?.() === false || organizationAnalysesSuspended) throw Object.assign(new Error("SIMY is stopping. Reopen SIMY before starting new work."), { code: "desktop_stopping" });
316
+ requireWorkAdmission();
306
317
  if (agent?.requireDesktopAccess) await agent.requireDesktopAccess();
307
318
  const access = await accessSnapshot();
308
- if (closed || stoppingForUpdate || updater?.acceptsNewWork?.() === false || organizationAnalysesSuspended) throw Object.assign(new Error("SIMY is stopping. Reopen SIMY before starting new work."), { code: "desktop_stopping" });
319
+ requireWorkAdmission();
309
320
  requireDesktopAccess(access);
310
321
  const scope = accessScope(access);
311
322
  if (expectedScope !== undefined && expectedScope !== scope) throw Object.assign(new Error("Desktop account or organization changed."), { code: "desktop_access_required" });
@@ -1327,7 +1338,14 @@ export async function startDesktopHost({ input = process.stdin, output = process
1327
1338
  const provider = PROVIDER_LINKS.includes(command.provider) ? command.provider : 'unsupported';
1328
1339
  const kind = ['Error', 'TypeError', 'RangeError', 'SyntaxError'].includes(error?.name) ? error.name : 'Error';
1329
1340
  const locations = [...String(error?.stack || '').matchAll(/(?:desktop-host|agent|provider-links|delivery-loop|analysis-manager)\.m?js:\d{1,6}:\d{1,6}/g)].slice(0, 3).map(match => match[0]).join(',') || 'unknown';
1330
- diagnostics.write(`provider connection failed provider=${provider} operation=${command.action} code=${code} kind=${kind} locations=${locations}`);
1341
+ const reason = code === 'desktop_stopping' && ['shutdown', 'update', 'organization_change'].includes(error?.stoppingReason) ? error.stoppingReason : 'unknown';
1342
+ diagnostics.write(`provider connection failed provider=${provider} operation=${command.action} code=${code} reason=${reason} kind=${kind} locations=${locations}`);
1343
+ // Admission can reject before changeProviderLink runs. Retain that
1344
+ // recovery guidance across the next heartbeat, without overwriting
1345
+ // an in-flight link change on the same provider.
1346
+ if (provider !== 'unsupported' && code === 'desktop_stopping' && !providerLinkBusy.has(provider)) {
1347
+ providerLinkFeedback[provider] = { state: 'error', message: desktopActionError(command.action, error) };
1348
+ }
1331
1349
  }
1332
1350
  emit({ type: "result", id: command.id, ok: false, ...(error?.code === 'cancelled' || statisticsRequest?.controller.signal.aborted ? { code: 'cancelled' } : {}), message: ["update", "prepare_update_restart"].includes(command.action) && updateProgress ? updateProgress.message : desktopActionError(command.action, error) });
1333
1351
  } finally {
@@ -1,6 +1,6 @@
1
1
  import spawn from "cross-spawn";
2
2
 
3
- import { BACKEND_VERSION_POLICIES, inspectBackendExecutable } from "./backend-executable.js";
3
+ import { BACKEND_VERSION_POLICIES, CODEX_WRITER_LOCK_MINIMUM_VERSION, inspectBackendExecutable } from "./backend-executable.js";
4
4
  import { isSessionValid } from "./session-store.js";
5
5
 
6
6
  const PROVIDER_STATUS_CACHE_MS = 30_000;
@@ -170,7 +170,9 @@ export async function inspectProviderStatus(provider, {
170
170
  const checkedAt = isoNow(now);
171
171
  let executable;
172
172
  try {
173
- executable = await inspectExecutable(provider, { spawnImpl, timeoutMs });
173
+ executable = await inspectExecutable(provider, { spawnImpl, timeoutMs,
174
+ ...(provider === "codex" ? { minimumVersion: CODEX_WRITER_LOCK_MINIMUM_VERSION } : {}),
175
+ });
174
176
  } catch {
175
177
  return providerStatus("unknown", null, checkedAt, `${label} availability could not be checked.`);
176
178
  }
@@ -13,7 +13,10 @@ export const DEV_WORK_EVENT_MAX_BYTES = 4096;
13
13
  const REQUEST_TIMEOUT_MS = 5_000;
14
14
 
15
15
  const REQUIRED_KEYS = ["repo", "branch", "head_sha", "session_id", "source"];
16
- const OPTIONAL_KEYS = ["pr_url", "project_id"];
16
+ // W-02: the independent WorkFlow run that started this coding session. Both
17
+ // or neither; Web/Crew keep it only when it is the device owner's own run.
18
+ const OPTIONAL_KEYS = ["pr_url", "project_id", "workflow_id", "workflow_run_id"];
19
+ const UUID_KEYS = ["project_id", "workflow_id", "workflow_run_id"];
17
20
  const ALLOWED_KEYS = new Set([...REQUIRED_KEYS, ...OPTIONAL_KEYS]);
18
21
  const SOURCES = new Set(["claude_code", "codex", "other"]);
19
22
  const REPO = /^[A-Za-z0-9_.-]{1,100}\/[A-Za-z0-9_.-]{1,100}$/;
@@ -52,14 +55,40 @@ export function normalizeDevWorkEvent(value) {
52
55
  !SESSION_ID.test(event.session_id) ||
53
56
  !SOURCES.has(event.source) ||
54
57
  (event.pr_url !== undefined && !PR_URL.test(event.pr_url)) ||
55
- (event.project_id !== undefined && !UUID.test(event.project_id))
58
+ UUID_KEYS.some((key) => event[key] !== undefined && !UUID.test(event[key])) ||
59
+ (event.workflow_id === undefined) !== (event.workflow_run_id === undefined)
56
60
  ) {
57
61
  throw invalid();
58
62
  }
59
- if (event.project_id !== undefined) event.project_id = event.project_id.toLowerCase();
63
+ for (const key of UUID_KEYS) {
64
+ if (event[key] !== undefined) event[key] = event[key].toLowerCase();
65
+ }
60
66
  return event;
61
67
  }
62
68
 
69
+ /** Environment names the SIMY plugin dev-work hook reads (hooks/dev-work.mjs). */
70
+ export const WORKFLOW_ID_ENV = "SIMY_WORKFLOW_ID";
71
+ export const WORKFLOW_RUN_ID_ENV = "SIMY_WORKFLOW_RUN_ID";
72
+
73
+ /**
74
+ * Provider child environment naming the independent WorkFlow run that started
75
+ * it, so the plugin hook can report which run the coding session belongs to.
76
+ * Without a complete run reference the keys are set to undefined, which
77
+ * child_process omits, so a value inherited from this agent's own environment
78
+ * can never attribute an unrelated session to a run.
79
+ */
80
+ export function workflowRunEnv(reference) {
81
+ const workflowId = String(reference?.workflow_id || "");
82
+ const runId = String(reference?.workflow_run_id || "");
83
+ if (!UUID.test(workflowId) || !UUID.test(runId)) {
84
+ return { [WORKFLOW_ID_ENV]: undefined, [WORKFLOW_RUN_ID_ENV]: undefined };
85
+ }
86
+ return {
87
+ [WORKFLOW_ID_ENV]: workflowId.toLowerCase(),
88
+ [WORKFLOW_RUN_ID_ENV]: runId.toLowerCase(),
89
+ };
90
+ }
91
+
63
92
  /**
64
93
  * Posts one normalized report to Web. Resolves with the minimal receipt; a
65
94
  * disabled gateway (404) or any refusal rejects with a status the local
package/src/local-task.js CHANGED
@@ -47,6 +47,7 @@ import {
47
47
  normalizeWorkflowStartIntent,
48
48
  } from "./workflow-observation-sender.js";
49
49
  import { uploadWorkflowDeliverables } from "./workflow-deliverable-upload.js";
50
+ import { workflowRunEnv } from "./dev-work-events.js";
50
51
 
51
52
  // Weighted tokens (cache reads count at 1/10). A real Claude Code WorkFlow step with the
52
53
  // Purpose Charter context measured ~200k weighted tokens on dev, so 100k failed every run.
@@ -580,6 +581,8 @@ export async function startLocalTask(
580
581
  TMP: runtimeTempRoot,
581
582
  TEMP: runtimeTempRoot,
582
583
  ...command.env,
584
+ // W-02: lets the dev-work hook link this session to its WorkFlow run.
585
+ ...workflowRunEnv(task.workflow_observation_binding),
583
586
  },
584
587
  stdio: ["ignore", "pipe", "pipe"],
585
588
  detached: process.platform !== "win32",
package/src/runner.js CHANGED
@@ -55,6 +55,7 @@ import { acquireDesktopProcessLock, PROCESS_CONFLICT_CODE } from "./desktop-proc
55
55
  import {
56
56
  assertRecoveryActor, completedWorkerResult, recoveryActorBinding, refreshRecoveryAudit, refreshRecoveryBudgetAudit,
57
57
  } from "./coding-run-recovery.js";
58
+ import { workflowRunEnv } from "./dev-work-events.js";
58
59
 
59
60
  const execFileAsync = promisify(execFile);
60
61
  const MAX_LOCAL_LOG_LINES = 1_000;
@@ -1035,6 +1036,17 @@ function sqmEvidenceEndpoint(session) {
1035
1036
  return new URL("sqm/run-evidence", session.api_base_url).href;
1036
1037
  }
1037
1038
 
1039
+ /**
1040
+ * The independent WorkFlow run this coding run belongs to: the bound run, or
1041
+ * the run a start intent already had admitted by an earlier attempt.
1042
+ */
1043
+ export function workflowRunReference(run) {
1044
+ const binding = run?.request?.workflow_observation_binding;
1045
+ if (binding) return binding;
1046
+ const observation = run?.snapshot?.metadata?.workflow_observation;
1047
+ return ["accepted", "already_recorded"].includes(observation?.status) ? observation : null;
1048
+ }
1049
+
1038
1050
  export async function executeProcessAttempt({
1039
1051
  backend,
1040
1052
  instruction,
@@ -1080,7 +1092,12 @@ export async function executeProcessAttempt({
1080
1092
  const checkpointWrites = [];
1081
1093
  const child = command.spawn(command.bin, command.args, {
1082
1094
  cwd: repositoryPath,
1083
- env: { ...process.env, ...command.env },
1095
+ env: {
1096
+ ...process.env,
1097
+ ...command.env,
1098
+ // W-02: lets the dev-work hook link this session to its WorkFlow run.
1099
+ ...workflowRunEnv(workflowRunReference(run)),
1100
+ },
1084
1101
  stdio: ["ignore", "pipe", "pipe"],
1085
1102
  detached: process.platform !== "win32",
1086
1103
  });