@awak-app/simy-cli 0.5.8 → 0.5.11

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 (32) hide show
  1. package/README.md +64 -0
  2. package/package.json +7 -3
  3. package/src/agent.js +70 -9
  4. package/src/auto-update.js +312 -35
  5. package/src/browser.js +24 -7
  6. package/src/cli-contract.js +1 -1
  7. package/src/codex-plugins/simy/skills/agentic-delivery-loop/SKILL.md +227 -18
  8. package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/acceptance-evidence.md +131 -0
  9. package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/change-boundary-matrix.md +20 -1
  10. package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/cross-repository-control.md +76 -0
  11. package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/delivery-levels.md +80 -0
  12. package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/minimum-completion.md +137 -0
  13. package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/shared-system-specifications.md +138 -0
  14. package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/state-and-receipts.md +18 -0
  15. package/src/codex-plugins/simy/skills/agentic-delivery-loop/scripts/check-acceptance.mjs +193 -0
  16. package/src/codex-plugins/simy/skills/agentic-delivery-loop/scripts/check-minimum-completion.mjs +221 -0
  17. package/src/console/app.js +21 -2
  18. package/src/console/commands.js +3 -0
  19. package/src/desktop-host.js +220 -0
  20. package/src/desktop-process-lock.js +231 -0
  21. package/src/desktop-status.js +263 -0
  22. package/src/direct-executor.js +6 -0
  23. package/src/index.js +37 -11
  24. package/src/install-update.js +494 -0
  25. package/src/orchestrator/audit.js +17 -2
  26. package/src/orchestrator/completion-contract.js +1 -1
  27. package/src/orchestrator/evidence.js +9 -1
  28. package/src/orchestrator/independent-audit.js +13 -1
  29. package/src/orchestrator/instruction.js +13 -1
  30. package/src/orchestrator/loop.js +34 -4
  31. package/src/orchestrator/minimum-completion.js +108 -0
  32. package/src/orchestrator/result.js +7 -0
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env node
2
+ // Record/observation checks, not authentication of the collector or host completion.
3
+ import { lstat, readFile, realpath } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { isDeepStrictEqual } from "node:util";
7
+ import {
8
+ checkAcceptance,
9
+ contextDigest,
10
+ readEvidenceArtifact,
11
+ sha256,
12
+ } from "./check-acceptance.mjs";
13
+
14
+ const text = (value) => typeof value === "string" && value.trim().length > 0;
15
+ const object = (value) =>
16
+ value !== null && typeof value === "object" && !Array.isArray(value);
17
+ export const MINIMUM_COMPLETION_VERSION = 1;
18
+
19
+ export function minimumCharter(charter) {
20
+ if (
21
+ charter?.completion_contract?.version !== 2 ||
22
+ !text(charter.id) ||
23
+ !text(charter.requirement) ||
24
+ !Array.isArray(charter.acceptance_criteria) ||
25
+ !charter.acceptance_criteria.length ||
26
+ !charter.acceptance_criteria.every(text)
27
+ ) {
28
+ throw new Error("Current completion charter required");
29
+ }
30
+ const source = {
31
+ original_request:
32
+ charter.thread_state?.original_request || charter.requirement,
33
+ requirement: charter.requirement,
34
+ minimum_scope:
35
+ charter.completion_contract.minimum_scope || charter.acceptance_criteria,
36
+ non_goals: charter.completion_contract.non_goals || [],
37
+ };
38
+ return {
39
+ schema_version: 1,
40
+ minimum_completion_version: MINIMUM_COMPLETION_VERSION,
41
+ run_id: charter.id,
42
+ source,
43
+ criteria: charter.acceptance_criteria.map((outcome, index) => ({
44
+ id: `AC-${index + 1}`,
45
+ outcome,
46
+ acceptance_source: `request:${contextDigest(source)}`,
47
+ layers: {
48
+ user: {
49
+ applicability: "required",
50
+ method:
51
+ "Exercise the actual operation and observe its result; reopen/reload when persistence is required.",
52
+ },
53
+ system: {
54
+ applicability: "required",
55
+ method:
56
+ "Independently inspect the final postcondition and necessary integration; persisted values and ownership when saving.",
57
+ },
58
+ },
59
+ })),
60
+ };
61
+ }
62
+
63
+ export function minimumContext(charter, headSha) {
64
+ const frozen = minimumCharter(charter);
65
+ return {
66
+ run_id: charter.id,
67
+ stage: "locally_verified",
68
+ environment: "local",
69
+ fixture_id: charter.id,
70
+ input_sha256: contextDigest(frozen.source),
71
+ source_revisions: { [charter.repository]: headSha },
72
+ };
73
+ }
74
+
75
+ export async function loadMinimumJson(file) {
76
+ const stat = await lstat(file);
77
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 * 1024)
78
+ throw new Error("Invalid minimum evidence input");
79
+ return JSON.parse(await readFile(file, "utf8"));
80
+ }
81
+
82
+ export async function checkMinimumCompletion({
83
+ charter,
84
+ matrix,
85
+ context,
86
+ evidenceRoot,
87
+ }) {
88
+ const frozen = minimumCharter(charter);
89
+ const charterBytes = Buffer.from(JSON.stringify(frozen));
90
+ const charterSha = sha256(charterBytes);
91
+ const contextSha = contextDigest(context);
92
+ const report = await checkAcceptance({
93
+ charterBytes,
94
+ matrix,
95
+ expectedCharterSha256: charterSha,
96
+ expectedContextSha256: contextSha,
97
+ evidenceRoot,
98
+ });
99
+ report.minimum_completion_version = MINIMUM_COMPLETION_VERSION;
100
+ report.charter_sha256 = charterSha;
101
+ report.context_sha256 = contextSha;
102
+ report.matrix_sha256 = contextDigest(matrix);
103
+ report.head_sha = context.source_revisions[charter.repository];
104
+ const fail = (id, reason) => report.errors.push(`${id}: ${reason}`);
105
+ for (const criterion of frozen.criteria) {
106
+ const row = matrix.results.find((result) => result.id === criterion.id);
107
+ if (
108
+ !Array.isArray(row?.implementation_refs) ||
109
+ !row.implementation_refs.length ||
110
+ !row.implementation_refs.every(
111
+ (ref) =>
112
+ text(ref) &&
113
+ !path.isAbsolute(ref) &&
114
+ !ref.includes("\\") &&
115
+ ref
116
+ .split("/")
117
+ .every(
118
+ (segment) =>
119
+ segment &&
120
+ segment !== ".." &&
121
+ segment !== "." &&
122
+ !segment.includes(":"),
123
+ ),
124
+ )
125
+ ) {
126
+ fail(
127
+ criterion.id,
128
+ "Missing or unsafe requirement-to-implementation mapping",
129
+ );
130
+ }
131
+ for (const [layer, names] of Object.entries({
132
+ user: ["operation", "result"],
133
+ system: ["postcondition", "integration"],
134
+ })) {
135
+ const cell = row?.layers?.[layer];
136
+ if (cell?.status !== "passed") continue;
137
+ try {
138
+ // The first artifact is a structured observation; further artifacts may be images/logs.
139
+ const observation = JSON.parse(
140
+ (
141
+ await readEvidenceArtifact(
142
+ await realpath(evidenceRoot),
143
+ cell.evidence[0],
144
+ )
145
+ ).toString(),
146
+ );
147
+ if (
148
+ observation.criterion_id !== criterion.id ||
149
+ observation.context_sha256 !== contextSha ||
150
+ !object(observation.checks)
151
+ )
152
+ throw new Error("Wrong observation context");
153
+ for (const name of names) {
154
+ const check = observation.checks[name];
155
+ if (
156
+ !object(check) ||
157
+ !text(check.subject) ||
158
+ check.expected === null ||
159
+ check.expected === undefined ||
160
+ (name === "operation" && check.expected !== true) ||
161
+ !isDeepStrictEqual(check.expected, check.actual)
162
+ ) {
163
+ fail(
164
+ `${criterion.id}/${layer}/${name}`,
165
+ "Missing observation or expected/actual mismatch",
166
+ );
167
+ }
168
+ }
169
+ } catch {
170
+ fail(
171
+ `${criterion.id}/${layer}`,
172
+ "Missing, altered or invalid structured observation",
173
+ );
174
+ }
175
+ }
176
+ }
177
+ report.status = report.errors.length
178
+ ? "evidence_incomplete"
179
+ : "evidence_complete";
180
+ // Additional floor failures must be visible in the human-readable record too.
181
+ report.markdown += `\nMinimum completion: ${report.status}\n${report.errors.map((error) => `- ${error}`).join("\n")}\n`;
182
+ return report;
183
+ }
184
+
185
+ const entry = process.argv[1]
186
+ ? await realpath(process.argv[1]).catch(() => null)
187
+ : null;
188
+ if (entry === fileURLToPath(import.meta.url)) {
189
+ try {
190
+ const [
191
+ charterPath,
192
+ matrixPath,
193
+ contextPath,
194
+ expectedCharter,
195
+ expectedContext,
196
+ ...extra
197
+ ] = process.argv.slice(2);
198
+ if (!expectedContext || extra.length) throw new Error("Invalid arguments");
199
+ const charter = await loadMinimumJson(charterPath);
200
+ const context = await loadMinimumJson(contextPath);
201
+ if (
202
+ sha256(Buffer.from(JSON.stringify(minimumCharter(charter)))) !==
203
+ expectedCharter ||
204
+ contextDigest(context) !== expectedContext
205
+ )
206
+ throw new Error("Trusted digest mismatch");
207
+ const report = await checkMinimumCompletion({
208
+ charter,
209
+ context,
210
+ matrix: await loadMinimumJson(matrixPath),
211
+ evidenceRoot: path.dirname(path.resolve(matrixPath)),
212
+ });
213
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
214
+ process.exitCode = report.status === "evidence_complete" ? 0 : 1;
215
+ } catch {
216
+ process.stderr.write(
217
+ "Minimum completion unavailable. Supply charter.json matrix.json context.json trusted-charter-sha256 trusted-context-sha256.\n",
218
+ );
219
+ process.exitCode = 2;
220
+ }
221
+ }
@@ -137,6 +137,23 @@ export function AgenticLoopConsole({ agent, onQuit = () => {} }) {
137
137
  }
138
138
  return;
139
139
  }
140
+ if (command.type === "update") {
141
+ setBusy(true);
142
+ try {
143
+ const result = await agent.updates?.performAction?.();
144
+ setNotice(
145
+ result?.opened
146
+ ? "Opened the SIMY Apple Silicon installer download page."
147
+ : `Open the installer download manually: ${cliUpdate?.action?.url || agent.webOrigin}`,
148
+ );
149
+ } catch (error) {
150
+ setNotice(error instanceof Error ? error.message : String(error));
151
+ } finally {
152
+ setBusy(false);
153
+ if (!selectedRun) setInputMode(true);
154
+ }
155
+ return;
156
+ }
140
157
  if (command.type === "configure") {
141
158
  setDraft((current) => updateDraft(current, command));
142
159
  setNotice(configurationNotice(command));
@@ -897,10 +914,12 @@ function ActionBar({
897
914
  wrap: "wrap",
898
915
  children: `UPDATE ${cliUpdate.message || "SIMY CLI update status changed."}`,
899
916
  }),
900
- cliUpdate.action?.command
917
+ cliUpdate.action?.command || cliUpdate.action?.url
901
918
  ? jsx(Text, {
902
919
  wrap: "wrap",
903
- children: `Next ${cliUpdate.action.command}`,
920
+ children: cliUpdate.action?.url
921
+ ? `Next /update or open ${cliUpdate.action.url}`
922
+ : `Next ${cliUpdate.action.command}`,
904
923
  })
905
924
  : null,
906
925
  ],
@@ -15,6 +15,7 @@ const HELP_LINES = [
15
15
  "/pause Pause the active executor process",
16
16
  "/resume Resume a paused executor process",
17
17
  "/stop Stop the selected run",
18
+ "/update Open the available installer download",
18
19
  ];
19
20
 
20
21
  export function parseConsoleCommand(value) {
@@ -66,6 +67,8 @@ export function parseConsoleCommand(value) {
66
67
  return { type: "resume" };
67
68
  case "/stop":
68
69
  return { type: "stop" };
70
+ case "/update":
71
+ return { type: "update" };
69
72
  case "/help":
70
73
  return { type: "help" };
71
74
  default:
@@ -0,0 +1,220 @@
1
+ import { spawn, execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { createInterface } from "node:readline";
4
+ import { fileURLToPath } from "node:url";
5
+ import { realpathSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ import { homedir } from "node:os";
8
+ import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
9
+ import { startAgent } from "./agent.js";
10
+ import { createCliAutoUpdater, readCliPackageInfo } from "./auto-update.js";
11
+ import { createAgentShutdown } from "./shutdown.js";
12
+ import { resolveWebOrigin } from "./web-origin.js";
13
+ import { openAuthorizationUrl } from "./browser.js";
14
+ import { inspectBackendExecutable } from "./backend-executable.js";
15
+ import { startWorkflowUpdates } from "./sqm/workflow-update.js";
16
+ import {
17
+ acquireDesktopProcessLock,
18
+ assertNoLegacyCliDaemon,
19
+ PROCESS_CONFLICT_CODE,
20
+ PROCESS_CONFLICT_MESSAGE,
21
+ validateDesktopSupervisor,
22
+ } from "./desktop-process-lock.js";
23
+
24
+ const silentLogger = { log() {}, error() {} };
25
+
26
+ export async function startDesktopHost({ input = process.stdin, output = process.stdout, env = process.env,
27
+ dependencies = {}, exit = (code) => { process.exitCode = code; } } = {}) {
28
+ const emit = (event) => output.write(`${JSON.stringify(event)}\n`);
29
+ const root = env.SIMY_HOME?.trim() || join(homedir(), ".simy");
30
+ const webOrigin = resolveWebOrigin(null, env);
31
+ const candidate = Boolean(env.SIMY_DESKTOP_SUPERVISOR_TOKEN);
32
+ if (candidate && !await validateDesktopSupervisor(root, env.SIMY_DESKTOP_SUPERVISOR_TOKEN)) {
33
+ throw new Error("Desktop update supervisor is no longer running.");
34
+ }
35
+ const lock = candidate ? null : await acquireDesktopProcessLock(root, { webOrigin });
36
+ const supervisorToken = lock?.nonce || env.SIMY_DESKTOP_SUPERVISOR_TOKEN;
37
+ let agent, updater, stopWorkflows = async () => {}, timer, closed = false, relay = null, publishing = null;
38
+ let shuttingDown = null;
39
+ let commands;
40
+ const packageInfo = dependencies.packageInfo || await readCliPackageInfo();
41
+ const publish = () => {
42
+ if (closed || relay || !agent) return Promise.resolve();
43
+ if (publishing) return publishing;
44
+ publishing = Promise.resolve(agent.getDesktopStatus()).then((status) => {
45
+ if (!closed && !relay) emit({ type: "status", status });
46
+ }).finally(() => { publishing = null; });
47
+ return publishing;
48
+ };
49
+ const stopAgent = async () => {
50
+ clearInterval(timer);
51
+ await stopWorkflows();
52
+ if (agent) await createAgentShutdown(agent)();
53
+ };
54
+ const close = () => shuttingDown ||= (async () => {
55
+ closed = true;
56
+ commands?.close();
57
+ input.pause?.();
58
+ if (relay) {
59
+ relay.stdin.end();
60
+ await new Promise((done) => {
61
+ const limit = setTimeout(() => { relay.kill("SIGTERM"); done(); }, 10_000);
62
+ relay.once("exit", () => { clearTimeout(limit); done(); });
63
+ if (relay.exitCode !== null) { clearTimeout(limit); done(); }
64
+ });
65
+ } else await stopAgent();
66
+ await lock?.release();
67
+ })();
68
+
69
+ try {
70
+ if (!candidate) {
71
+ await (dependencies.assertNoLegacyDaemon || assertNoLegacyCliDaemon)(webOrigin);
72
+ }
73
+ updater = await (dependencies.createUpdater || createCliAutoUpdater)({
74
+ daemon: true, interactive: true, webOrigin,
75
+ disabled: env.SIMY_AUTO_UPDATE === "0",
76
+ dependencies: { packageInfo, logger() {} },
77
+ });
78
+ agent = await (dependencies.startAgent || startAgent)({
79
+ daemon: true, webOrigin, sessionRoot: root, quiet: true, updateManager: updater,
80
+ });
81
+ stopWorkflows = (dependencies.startWorkflowUpdates || startWorkflowUpdates)({ enabled: env.SIMY_AUTO_UPDATE !== "0" });
82
+ updater.on?.("change", () => { void publish().catch(() => {}); });
83
+ updater.start({ registry: agent.registry, restartDaemon: async (targetVersion, installation) => {
84
+ if (!installation?.entryPath || !installation?.executablePath) throw new Error("Signed update runtime is unavailable.");
85
+ const child = (dependencies.spawn || spawn)(installation.executablePath,
86
+ [join(dirname(installation.entryPath), "desktop-host.js")], {
87
+ stdio: ["pipe", "pipe", "ignore"],
88
+ env: { ...env, SIMY_DESKTOP_SUPERVISOR_TOKEN: supervisorToken },
89
+ });
90
+ const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
91
+ let accepted = false;
92
+ child.stdin.on("error", () => {
93
+ if (relay === child && !closed) {
94
+ emit({ type: "error", message: "SIMY update connection ended. Reconnect to start the service again." });
95
+ void close().then(() => exit(1));
96
+ }
97
+ });
98
+ child.on("exit", () => {
99
+ if (relay === child && !closed) {
100
+ emit({ type: "error", message: "SIMY stopped. Reconnect to start it again." });
101
+ void close().then(() => exit(1));
102
+ }
103
+ });
104
+ try {
105
+ const first = await new Promise((accept, reject) => {
106
+ const limit = setTimeout(() => reject(new Error("Updated desktop runtime did not start in time.")), 30_000);
107
+ const finish = (fn, value) => { clearTimeout(limit); fn(value); };
108
+ child.once("error", () => finish(reject, new Error("Updated desktop runtime could not start.")));
109
+ child.once("exit", () => { if (!accepted) finish(reject, new Error("Updated desktop runtime exited before verification.")); });
110
+ lines.on("line", (line) => {
111
+ if (line.length > 131072) return;
112
+ let event; try { event = JSON.parse(line); } catch { return; }
113
+ if (accepted) { emit(event); return; }
114
+ if (event.type === "status") {
115
+ if (!validCandidateStatus(event.status, targetVersion, webOrigin)) {
116
+ finish(reject, new Error("Updated desktop runtime identity mismatch."));
117
+ } else { accepted = true; finish(accept, event); }
118
+ }
119
+ });
120
+ });
121
+ relay = child;
122
+ await stopAgent();
123
+ if (child.exitCode !== null || closed) throw new Error("Updated runtime stopped during handoff.");
124
+ emit(first);
125
+ } catch (error) {
126
+ lines.close(); child.kill("SIGTERM"); throw error;
127
+ }
128
+ } });
129
+ await publish();
130
+ timer = setInterval(() => { void publish().catch(() => emit({ type: "error", message: "SIMY status could not be refreshed." })); }, 3_000);
131
+ } catch (error) { await close(); throw error; }
132
+
133
+ let queue = Promise.resolve();
134
+ commands = createInterface({ input, crlfDelay: Infinity });
135
+ commands.on("line", (line) => {
136
+ if (closed) return;
137
+ if (line.length > 4096) return;
138
+ let command; try { command = JSON.parse(line); } catch { return; }
139
+ if (typeof command.id !== "string" || command.id.length > 128) return;
140
+ if (relay) {
141
+ if (!relay.stdin.destroyed && relay.exitCode === null) relay.stdin.write(`${JSON.stringify(command)}\n`);
142
+ return;
143
+ }
144
+ queue = queue.then(async () => {
145
+ try {
146
+ const action = command.action;
147
+ if (action === "refresh" || action === "reconnect") {
148
+ await agent.refreshCodexStatus?.();
149
+ await agent.refreshDesktopStatus();
150
+ }
151
+ else if (action === "login") {
152
+ const url = await agent.requestDesktopLogin();
153
+ if (url) {
154
+ if (new URL(url).origin !== webOrigin) throw new Error("Invalid login origin.");
155
+ const result = await (dependencies.openLogin || openAuthorizationUrl)(url, { logger: silentLogger });
156
+ if (!result.opened) throw new Error("Could not open the login page.");
157
+ }
158
+ } else if (action === "check_update") await updater.checkNow();
159
+ else if (action === "update") {
160
+ if (updater.snapshot().state === "waiting_for_idle") await updater.maybeApply();
161
+ else await updater.performAction();
162
+ } else if (action === "codex_login") await (dependencies.codexLogin || openCodexLogin)(root);
163
+ else if (action === "quit") {
164
+ const status = await agent.getDesktopStatus();
165
+ if (status.tasks.activeCount > 0 && command.force !== true) throw new Error("Tasks are still running. Confirm before quitting.");
166
+ emit({ type: "result", id: command.id, ok: true });
167
+ await close(); exit(0); return;
168
+ } else throw new Error("Unsupported desktop action.");
169
+ emit({ type: "result", id: command.id, ok: true });
170
+ await publish();
171
+ } catch {
172
+ emit({ type: "result", id: command.id, ok: false, message: "Action could not finish. Refresh the status and try again; active tasks must finish before quitting." });
173
+ }
174
+ }).catch(() => {});
175
+ });
176
+ commands.on("close", () => { if (!closed) void close().then(() => exit(0)); });
177
+ return { close, publish, agent };
178
+ }
179
+
180
+ function validCandidateStatus(status, version, webOrigin) {
181
+ return status?.schemaVersion === 1 && status.version === version && status.webOrigin === webOrigin
182
+ && ["signed_out", "signed_in", "expired"].includes(status.auth?.state)
183
+ && ["connecting", "online", "offline", "unauthorized", "unknown"].includes(status.connection?.state)
184
+ && Number.isSafeInteger(status.tasks?.activeCount) && status.tasks.activeCount >= 0
185
+ && Array.isArray(status.tasks.items) && typeof status.codex?.state === "string";
186
+ }
187
+
188
+ async function openCodexLogin(root) {
189
+ const codex = await inspectBackendExecutable("codex");
190
+ if (!codex.available || !codex.executable) throw new Error("Codex is not installed.");
191
+ const directory = await mkdtemp(join(root, "codex-login-"));
192
+ const script = join(directory, "Sign in to Codex.command");
193
+ const quote = (s) => `'${s.replaceAll("'", "'\\''")}'`;
194
+ await writeFile(script, `#!/bin/bash\nexport PATH="/opt/homebrew/bin:/usr/local/bin:$HOME/.local/bin:$PATH"\n${quote(codex.executable)} login\nresult=$?\nrm -f -- ${quote(script)}\nrmdir -- ${quote(directory)}\nexit "$result"\n`, { mode: 0o700 });
195
+ await chmod(script, 0o700);
196
+ try { await promisify(execFile)("/usr/bin/open", ["-a", "Terminal", script], { timeout: 10_000 }); }
197
+ catch (error) { await rm(directory, { recursive: true, force: true }); throw error; }
198
+ }
199
+
200
+ export function isDesktopHostEntry(entry = process.argv[1]) {
201
+ try { return Boolean(entry) && realpathSync(entry) === fileURLToPath(import.meta.url); }
202
+ catch { return false; }
203
+ }
204
+
205
+ if (isDesktopHostEntry()) {
206
+ let host;
207
+ const stop = () => { void host?.close().finally(() => process.exit(0)); };
208
+ process.once("SIGTERM", stop); process.once("SIGINT", stop);
209
+ startDesktopHost().then((value) => { host = value; }).catch((error) => {
210
+ const conflict = error?.code === PROCESS_CONFLICT_CODE;
211
+ process.stdout.write(`${JSON.stringify({
212
+ type: "error",
213
+ ...(conflict ? { code: PROCESS_CONFLICT_CODE } : {}),
214
+ message: conflict
215
+ ? PROCESS_CONFLICT_MESSAGE
216
+ : "SIMY could not start. Check whether another SIMY window is already running, then reconnect.",
217
+ })}\n`);
218
+ process.exitCode = 1;
219
+ });
220
+ }