@awak-app/simy-cli 0.5.17 → 0.5.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.5.17",
3
+ "version": "0.5.18",
4
4
  "description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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.5.17";
16
+ export const CLI_VERSION = "0.5.18";
17
17
  export const CLI_API_CONTRACT_VERSION = 10;
18
18
 
19
19
  export function withCliContract(
@@ -210,7 +210,7 @@ if (isDesktopHostEntry()) {
210
210
  const conflict = error?.code === PROCESS_CONFLICT_CODE;
211
211
  process.stdout.write(`${JSON.stringify({
212
212
  type: "error",
213
- ...(conflict ? { code: PROCESS_CONFLICT_CODE } : {}),
213
+ ...(conflict ? { code: PROCESS_CONFLICT_CODE, conflicts: error.conflicts || [] } : {}),
214
214
  message: conflict
215
215
  ? PROCESS_CONFLICT_MESSAGE
216
216
  : "SIMY could not start. Check whether another SIMY window is already running, then reconnect.",
@@ -10,10 +10,11 @@ export const PROCESS_CONFLICT_MESSAGE =
10
10
  "Another SIMY background service is already running for this Web address. Stop the existing CLI daemon, then reopen SIMY CLI.";
11
11
 
12
12
  export class SimyProcessConflictError extends Error {
13
- constructor(message = PROCESS_CONFLICT_MESSAGE) {
13
+ constructor(message = PROCESS_CONFLICT_MESSAGE, conflicts = []) {
14
14
  super(message);
15
15
  this.name = "SimyProcessConflictError";
16
16
  this.code = PROCESS_CONFLICT_CODE;
17
+ this.conflicts = conflicts;
17
18
  }
18
19
  }
19
20
 
@@ -96,12 +97,13 @@ export async function assertNoDesktopHostForOrigin(
96
97
  throw new SimyProcessConflictError();
97
98
  }
98
99
 
99
- export async function assertNoLegacyCliDaemon(
100
+ export async function findLegacyCliDaemons(
100
101
  webOrigin,
101
102
  {
102
103
  processes = listSameUserProcesses,
103
104
  verifyEntry = verifySimyCliEntry,
104
105
  currentPid = process.pid,
106
+ identity = processStart,
105
107
  } = {},
106
108
  ) {
107
109
  const requestedOrigin = normalizeWebOrigin(webOrigin);
@@ -113,13 +115,48 @@ export async function assertNoLegacyCliDaemon(
113
115
  "SIMY could not verify whether another CLI service is running. Stop the existing CLI daemon, then reopen SIMY CLI.",
114
116
  );
115
117
  }
118
+ const conflicts = [];
116
119
  for (const processInfo of candidates) {
117
- if (processInfo.pid === currentPid) continue;
120
+ if (!Number.isSafeInteger(processInfo.pid) || processInfo.pid <= 0 || processInfo.pid === currentPid) continue;
118
121
  const parsed = parseLegacyAgentArguments(processInfo.command ?? processInfo.args);
119
122
  if (!parsed || !(await verifyEntry(parsed.entryPath))) continue;
120
123
  if (parsed.webOrigin && parsed.webOrigin !== requestedOrigin) continue;
121
- throw new SimyProcessConflictError();
124
+ const startedAt = await identity(processInfo.pid).catch(() => null);
125
+ conflicts.push({ pid: processInfo.pid, entryPath: parsed.entryPath, startedAt });
122
126
  }
127
+ return conflicts;
128
+ }
129
+
130
+
131
+ export async function assertNoLegacyCliDaemon(webOrigin, options) {
132
+ const conflicts = await findLegacyCliDaemons(webOrigin, options);
133
+ if (conflicts.length) throw new SimyProcessConflictError(PROCESS_CONFLICT_MESSAGE, conflicts);
134
+ }
135
+
136
+ // Re-discover same-user SIMY processes and compare their start time immediately
137
+ // before signaling. A PID alone is never sufficient authorization after a dialog.
138
+ export async function stopConfirmedCliConflicts(webOrigin, approved, { force = false,
139
+ discover = findLegacyCliDaemons, signal = process.kill.bind(process),
140
+ alive = processAlive, wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
141
+ } = {}) {
142
+ if (!Array.isArray(approved) || !approved.length || approved.length > 100) throw new Error("No confirmed SIMY process.");
143
+ const stopped = [];
144
+ for (const expected of approved) {
145
+ if (!Number.isSafeInteger(expected.pid) || expected.pid <= 0 || !expected.startedAt || typeof expected.entryPath !== "string") {
146
+ throw new Error("SIMY process identity is unavailable. Refresh and try again.");
147
+ }
148
+ const current = (await discover(webOrigin)).find((item) => item.pid === expected.pid);
149
+ if (!current) continue;
150
+ if (current.startedAt !== expected.startedAt || current.entryPath !== expected.entryPath) {
151
+ throw new Error("The process changed. Refresh before stopping it.");
152
+ }
153
+ try { signal(current.pid, force ? "SIGKILL" : "SIGTERM"); }
154
+ catch (error) { if (error.code !== "ESRCH") throw error; }
155
+ stopped.push(current);
156
+ }
157
+ for (let n = 0; n < 40 && stopped.some((item) => alive(item.pid)); n++) await wait(250);
158
+ const remaining = await discover(webOrigin);
159
+ return { remaining: remaining.filter((item) => stopped.some((old) => old.pid === item.pid && old.startedAt === item.startedAt && old.entryPath === item.entryPath)) };
123
160
  }
124
161
 
125
162
  export function parseLegacyAgentArguments(args) {
@@ -0,0 +1,17 @@
1
+ import { stopConfirmedCliConflicts } from "./desktop-process-lock.js";
2
+ import { resolveWebOrigin } from "./web-origin.js";
3
+
4
+ // Invoked by the native confirmation dialog, never automatically at startup.
5
+ let input = "";
6
+ try {
7
+ for await (const chunk of process.stdin) {
8
+ input += chunk;
9
+ if (input.length > 32768) throw new Error("Confirmation is too large.");
10
+ }
11
+ const request = JSON.parse(input);
12
+ const result = await stopConfirmedCliConflicts(resolveWebOrigin(null, process.env), request.conflicts, { force: request.force === true });
13
+ process.stdout.write(JSON.stringify({ ok: true, ...result }));
14
+ } catch {
15
+ process.stdout.write(JSON.stringify({ ok: false }));
16
+ process.exitCode = 1;
17
+ }