@awak-app/simy-cli 0.5.7 → 0.5.10

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.
@@ -9,11 +9,18 @@ Persist one controller-owned snapshot with at least:
9
9
 
10
10
  - `run_id`, `state`, monotonic `revision`, and terminal disposition;
11
11
  - immutable explicit conditions and their stable digest;
12
+ - acceptance-source identities and digests, architecture boundaries, applicable
13
+ rule decisions, risk tier, requested/effective delivery levels, the safety
14
+ ceiling reason, and chat-confirmation receipt;
12
15
  - repository, base SHA, head SHA, companion SHAs, merge SHA, image digests, and
13
16
  deployed revision;
14
17
  - target environments, user journey, user/tenant fixture identities, and
15
18
  required artifacts;
16
19
  - applicable gates and their current status;
20
+ - frozen acceptance charter digest, current matrix/table paths and digests,
21
+ superseded matrix snapshots, and the acceptance validator result for each gate;
22
+ - verification-boundary changes, deferred-debt references, and applicable
23
+ outcome and guardrail measures;
17
24
  - attempt number, retry/time/cost budgets, and failure fingerprints;
18
25
  - append-only receipt IDs, evidence references, timestamps, and cleanup state.
19
26
 
@@ -29,6 +36,7 @@ Use the smallest applicable receipts rather than free-form completion claims:
29
36
  - `FailureReproductionReceipt`
30
37
  - `ImplementationReceipt`
31
38
  - `LocalVerificationReceipt`
39
+ - `AcceptanceEvidenceReceipt` (matrix completeness, not overall completion)
32
40
  - `IndependentReviewReceipt`
33
41
  - `PRCIReceipt`
34
42
  - `IntegrationVerificationReceipt`
@@ -91,6 +99,7 @@ Evidence is revision-bound:
91
99
  | Change | Invalidate |
92
100
  |---|---|
93
101
  | Charter or explicit success conditions | All affected downstream gates |
102
+ | Acceptance source, test oracle, assertion, or CI/security gate changes | The affected gate and all dependent downstream evidence |
94
103
  | Head SHA or generated source changes | Local, CI, integration, container, and live evidence |
95
104
  | Base SHA or companion revision changes | Integration and later evidence |
96
105
  | Container image digest changes | Container user journey and later runtime evidence |
@@ -125,6 +134,7 @@ For the full delivery mode:
125
134
  ```text
126
135
  done =
127
136
  reproduction_verified
137
+ && acceptance_evidence_complete
128
138
  && locally_verified
129
139
  && pr_ci_green
130
140
  && integration_verified
@@ -140,3 +150,8 @@ done =
140
150
  Remove only genuinely non-applicable terms and record why. If merge or deploy
141
151
  is outside current authority, stop at the highest proven pre-authority state.
142
152
 
153
+ Acceptance evidence must cover every applicable criterion for the target gate,
154
+ with current context and the required user/system layers. After each evaluated
155
+ action, refresh the JSON matrix and its readable table; append a receipt for the
156
+ update. Keep the frozen charter distinct from mutable results. A charter patch
157
+ supersedes the old digest only after the controller verifies its authority.
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Offline record validation only. The controller owns acceptance and provenance.
4
+ import { createHash } from "node:crypto";
5
+ import { constants } from "node:fs";
6
+ import { lstat, open, readFile, realpath } from "node:fs/promises";
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const layers = ["user", "system"];
11
+ const hashPattern = /^[a-f0-9]{64}$/;
12
+ const text = (value) => typeof value === "string" && value.trim().length > 0;
13
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
14
+ export const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
15
+ export const contextDigest = (context) => sha256(JSON.stringify(canonical(context)));
16
+
17
+ function canonical(value) {
18
+ if (Array.isArray(value)) return value.map(canonical);
19
+ if (!object(value)) return value;
20
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
21
+ }
22
+
23
+ function requireValue(condition, message) {
24
+ if (!condition) throw new Error(message);
25
+ }
26
+
27
+ function validateCharter(charter) {
28
+ requireValue(object(charter) && charter.schema_version === 1 && text(charter.run_id), "Invalid charter header");
29
+ requireValue(Array.isArray(charter.criteria) && charter.criteria.length > 0, "Charter requires criteria");
30
+ const ids = new Set();
31
+ for (const criterion of charter.criteria) {
32
+ requireValue(object(criterion) && text(criterion.id) && !ids.has(criterion.id), "Duplicate or invalid criterion ID");
33
+ ids.add(criterion.id);
34
+ requireValue(text(criterion.outcome) && text(criterion.acceptance_source), "Criterion needs outcome and independent acceptance source");
35
+ requireValue(object(criterion.layers), "Criterion needs verification layers");
36
+ let required = 0;
37
+ for (const layer of layers) {
38
+ const spec = criterion.layers[layer];
39
+ requireValue(object(spec), `Missing ${layer} layer specification`);
40
+ if (spec.applicability === "required") {
41
+ requireValue(text(spec.method), "Required layer needs a verification method");
42
+ required += 1;
43
+ } else {
44
+ requireValue(spec.applicability === "not_applicable" && text(spec.reason) && text(spec.source), "Non-applicability needs a reason and source in the charter");
45
+ }
46
+ }
47
+ requireValue(required > 0, "A criterion cannot waive both verification layers");
48
+ }
49
+ }
50
+
51
+ function validateContext(context, runId) {
52
+ requireValue(object(context) && context.run_id === runId, "Context run ID mismatch");
53
+ for (const field of ["stage", "environment", "fixture_id"]) {
54
+ requireValue(text(context[field]), `Context needs ${field}`);
55
+ }
56
+ requireValue(hashPattern.test(context.input_sha256), "Context needs input SHA-256");
57
+ requireValue(object(context.source_revisions) && Object.keys(context.source_revisions).length > 0, "Context needs repository revisions");
58
+ for (const [repository, revision] of Object.entries(context.source_revisions)) {
59
+ requireValue(text(repository) && typeof revision === "string" && /^[a-f0-9]{40}$/.test(revision), "Invalid repository revision");
60
+ }
61
+ }
62
+
63
+ async function checkArtifact(root, artifact) {
64
+ requireValue(object(artifact) && text(artifact.path) && hashPattern.test(artifact.sha256), "Invalid artifact reference");
65
+ // Explicit bundle-relative paths only; never fetch URLs or execute evidence.
66
+ const segments = artifact.path.split("/");
67
+ requireValue(!path.isAbsolute(artifact.path) && !artifact.path.includes("\\") &&
68
+ segments.every((part) => part && part !== "." && part !== ".." && !part.includes(":")), "Unsafe artifact path");
69
+ let target = root;
70
+ for (let index = 0; index < segments.length; index += 1) {
71
+ target = path.join(target, segments[index]);
72
+ const stat = await lstat(target);
73
+ requireValue(!stat.isSymbolicLink(), "Symbolic evidence paths are not allowed");
74
+ if (index < segments.length - 1) requireValue(stat.isDirectory(), "Invalid evidence directory");
75
+ }
76
+ const handle = await open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
77
+ try {
78
+ const stat = await handle.stat();
79
+ requireValue(stat.isFile() && stat.size > 0 && stat.size <= 16 * 1024 * 1024, "Artifact must be a nonempty regular file of at most 16 MiB");
80
+ requireValue(sha256(await handle.readFile()) === artifact.sha256, "Artifact digest mismatch");
81
+ } finally {
82
+ await handle.close();
83
+ }
84
+ }
85
+
86
+ const tableText = (value) => String(value ?? "").replaceAll("|", "\\|").replace(/[\r\n]+/g, " ");
87
+
88
+ export function renderChecklist(report) {
89
+ const header = [
90
+ "| Criterion / outcome | Layer / method | Status | Observation | Evidence | Unmet reason / next action |",
91
+ "| --- | --- | --- | --- | --- | --- |",
92
+ ];
93
+ const rows = report.checks.map((row) => "| " + [
94
+ `${row.id}: ${row.outcome}`, `${row.layer}: ${row.method}`, row.status,
95
+ row.observation, row.evidence.join(", "), [row.reason, row.next_action].filter(Boolean).join(" / "),
96
+ ].map(tableText).join(" | ") + " |");
97
+ return [...header, ...rows, "", `Validator: ${report.status}`,
98
+ ...report.errors.map((error) => `- ${tableText(error)}`)].join("\n") + "\n";
99
+ }
100
+
101
+ export async function checkAcceptance({ charterBytes, matrix, expectedCharterSha256, expectedContextSha256, evidenceRoot }) {
102
+ requireValue(hashPattern.test(expectedCharterSha256) && hashPattern.test(expectedContextSha256), "Trusted charter and context SHA-256 values are required");
103
+ requireValue(sha256(charterBytes) === expectedCharterSha256, "Frozen charter digest mismatch");
104
+ const charter = JSON.parse(charterBytes.toString());
105
+ validateCharter(charter);
106
+ requireValue(object(matrix) && matrix.schema_version === 1, "Invalid matrix schema version");
107
+ requireValue(matrix.charter_sha256 === expectedCharterSha256, "Matrix charter digest mismatch");
108
+ validateContext(matrix.context, charter.run_id);
109
+ requireValue(contextDigest(matrix.context) === expectedContextSha256, "Current context digest mismatch");
110
+ requireValue(Array.isArray(matrix.results), "Matrix requires results");
111
+ const root = await realpath(evidenceRoot);
112
+ const report = { status: "evidence_incomplete", checks: [], errors: [] };
113
+ const results = new Map();
114
+ const expectedIds = new Set(charter.criteria.map((criterion) => criterion.id));
115
+ for (const row of matrix.results) {
116
+ requireValue(object(row) && expectedIds.has(row.id) && !results.has(row.id), "Duplicate or unknown result criterion");
117
+ requireValue(object(row.layers) && Object.keys(row.layers).every((key) => layers.includes(key)), "Invalid result layers");
118
+ results.set(row.id, row);
119
+ }
120
+ for (const criterion of charter.criteria) {
121
+ for (const layer of layers) {
122
+ const spec = criterion.layers[layer];
123
+ const cell = results.get(criterion.id)?.layers[layer];
124
+ const row = { id: criterion.id, outcome: criterion.outcome, layer, method: spec.method ?? spec.reason,
125
+ status: "pending", observation: "", evidence: [], reason: "", next_action: "" };
126
+ report.checks.push(row);
127
+ if (spec.applicability === "not_applicable") {
128
+ row.status = "not_applicable";
129
+ row.reason = `${spec.reason} (${spec.source})`;
130
+ continue;
131
+ }
132
+ if (!object(cell)) {
133
+ row.reason = "Missing result";
134
+ row.next_action = "Run the specified check and record evidence";
135
+ } else {
136
+ row.observation = text(cell.observation) ? cell.observation : "";
137
+ row.next_action = text(cell.next_action) ? cell.next_action : "";
138
+ row.evidence = Array.isArray(cell.evidence) ? cell.evidence.map((entry) => object(entry) && text(entry.path) ? entry.path : "invalid reference") : [];
139
+ if (!["passed", "failed", "pending"].includes(cell.status)) {
140
+ row.reason = "Invalid status (results cannot waive a required layer)";
141
+ } else if (cell.context_sha256 !== expectedContextSha256 || cell.charter_sha256 !== expectedCharterSha256) {
142
+ row.reason = "Stale result; reverify for the current charter and context";
143
+ } else if (cell.status !== "passed") {
144
+ row.status = cell.status;
145
+ row.reason = text(cell.reason) ? cell.reason : "Missing unmet reason";
146
+ if (!row.next_action) row.reason += "; missing next action";
147
+ } else {
148
+ try {
149
+ requireValue(text(cell.action) && text(cell.observation), "Passed result needs action and observation");
150
+ requireValue(text(cell.observed_at) && /^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/.test(cell.observed_at) &&
151
+ Number.isFinite(Date.parse(cell.observed_at)) && Date.parse(cell.observed_at) <= Date.now(), "Invalid or future observation timestamp");
152
+ requireValue(Array.isArray(cell.evidence) && cell.evidence.length > 0, "Passed result needs evidence");
153
+ for (const artifact of cell.evidence) await checkArtifact(root, artifact);
154
+ row.status = "passed";
155
+ } catch {
156
+ row.reason = "Invalid, missing, empty, unsafe, or changed evidence; check action, observation, timestamp and artifact hashes";
157
+ }
158
+ }
159
+ }
160
+ if (row.status !== "passed") report.errors.push(`${criterion.id}/${layer}: ${row.reason}`);
161
+ }
162
+ }
163
+ if (!report.errors.length) report.status = "evidence_complete";
164
+ report.markdown = renderChecklist(report);
165
+ return report;
166
+ }
167
+
168
+ async function main(args) {
169
+ requireValue(args.length === 4 || (args.length === 5 && args[4] === "--markdown"),
170
+ "Usage: node check-acceptance.mjs <charter.json> <matrix.json> <trusted-charter-sha256> <current-context-sha256> [--markdown]");
171
+ const [charterPath, matrixPath, expectedCharterSha256, expectedContextSha256] = args;
172
+ const load = async (target) => {
173
+ const stat = await lstat(target);
174
+ requireValue(stat.isFile() && stat.size <= 1024 * 1024, "Input must be a regular JSON file of at most 1 MiB");
175
+ return readFile(target);
176
+ };
177
+ const report = await checkAcceptance({ charterBytes: await load(charterPath),
178
+ matrix: JSON.parse((await load(matrixPath)).toString()), expectedCharterSha256,
179
+ expectedContextSha256, evidenceRoot: path.dirname(path.resolve(matrixPath)) });
180
+ process.stdout.write(args[4] === "--markdown" ? report.markdown : JSON.stringify(report, null, 2) + "\n");
181
+ process.exitCode = report.status === "evidence_complete" ? 0 : 1;
182
+ }
183
+
184
+ const entryPath = process.argv[1] ? await realpath(process.argv[1]).catch(() => null) : null;
185
+ if (entryPath === fileURLToPath(import.meta.url)) {
186
+ main(process.argv.slice(2)).catch(() => {
187
+ // Do not echo arbitrary JSON, filesystem contents, or sensitive input values.
188
+ process.stderr.write("Acceptance input invalid. Check JSON schema, paths, frozen charter and current context digests. Usage: node check-acceptance.mjs <charter.json> <matrix.json> <trusted-charter-sha256> <current-context-sha256> [--markdown]\n");
189
+ process.exitCode = 2;
190
+ });
191
+ }
@@ -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
+ }