pmx-canvas 0.1.1 → 0.1.2

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.
@@ -163,6 +163,12 @@ async function runProcess(
163
163
  ]).has(key);
164
164
  }),
165
165
  );
166
+ // Spawn in its own process group (POSIX only — Windows has a different model).
167
+ // This lets us kill every descendant — pnpm, bun, parcel, swc, lmdb, etc. —
168
+ // if the build hangs or times out, instead of leaving orphans that accumulate
169
+ // file descriptors and processes across retries (seen as later
170
+ // `fork: Resource temporarily unavailable` in end-user reports).
171
+ const isPosix = process.platform !== 'win32';
166
172
  const child = spawn(command, args, {
167
173
  cwd: options.cwd,
168
174
  env: {
@@ -171,10 +177,33 @@ async function runProcess(
171
177
  CI: '1',
172
178
  npm_config_yes: 'true',
173
179
  pnpm_config_yes: 'true',
180
+ // Cap pnpm's internal child concurrency so installs don't blow past
181
+ // macOS default ulimit -u (often 256-2048) when resolving the ~30
182
+ // radix-ui dependencies in one `pnpm add` call.
183
+ pnpm_config_child_concurrency: '2',
184
+ NPM_CONFIG_CHILD_CONCURRENCY: '2',
174
185
  },
175
186
  stdio: ['ignore', 'pipe', 'pipe'],
187
+ detached: isPosix,
176
188
  });
177
189
 
190
+ const killTree = (signal: NodeJS.Signals): void => {
191
+ if (isPosix && typeof child.pid === 'number') {
192
+ try {
193
+ // Negative pid = send to the whole process group.
194
+ process.kill(-child.pid, signal);
195
+ return;
196
+ } catch {
197
+ // fall through to direct kill
198
+ }
199
+ }
200
+ try {
201
+ child.kill(signal);
202
+ } catch {
203
+ // ignore
204
+ }
205
+ };
206
+
178
207
  let stdout = '';
179
208
  let stderr = '';
180
209
  let timedOut = false;
@@ -189,12 +218,13 @@ async function runProcess(
189
218
  await new Promise<void>((resolvePromise, rejectPromise) => {
190
219
  const timer = setTimeout(() => {
191
220
  timedOut = true;
192
- child.kill('SIGKILL');
221
+ killTree('SIGKILL');
193
222
  rejectPromise(new Error(`Command timed out after ${options.timeoutMs}ms: ${command}`));
194
223
  }, options.timeoutMs);
195
224
 
196
225
  child.on('error', (error) => {
197
226
  clearTimeout(timer);
227
+ killTree('SIGKILL');
198
228
  rejectPromise(error);
199
229
  });
200
230
 
@@ -202,9 +232,17 @@ async function runProcess(
202
232
  clearTimeout(timer);
203
233
  if (timedOut) return;
204
234
  if (code !== 0) {
235
+ const trimmedStderr = stderr.trim();
236
+ const stderrTail = trimmedStderr.split('\n').slice(-20).join('\n');
237
+ const trimmedStdout = stdout.trim();
238
+ const stdoutTail = trimmedStdout.split('\n').slice(-20).join('\n');
205
239
  rejectPromise(
206
240
  new Error(
207
- [`Command failed (${code}): ${command} ${args.join(' ')}`, stderr.trim()]
241
+ [
242
+ `Command failed (${code}): ${command} ${args.join(' ')}`,
243
+ stderrTail && `stderr:\n${stderrTail}`,
244
+ !trimmedStderr && stdoutTail && `stdout:\n${stdoutTail}`,
245
+ ]
208
246
  .filter(Boolean)
209
247
  .join('\n'),
210
248
  ),
@@ -74,3 +74,28 @@ export function normalizeExtAppToolResult(
74
74
  isError,
75
75
  };
76
76
  }
77
+
78
+ /**
79
+ * Structural equality between two `CallToolResult` values, used by the host
80
+ * ExtAppFrame to suppress echo-back re-renders when an SSE layout update
81
+ * mints a new object reference for an otherwise-unchanged tool result.
82
+ *
83
+ * JSON-stringify is adequate here: tool results are strictly JSON (no
84
+ * functions, symbols, or cycles), typically small, and on the hot path we
85
+ * only hit this when references already differ. For very large payloads
86
+ * (> ~2MB) an early length check skips the stringify to avoid a user-visible
87
+ * stall — such results are treated as "changed" and forwarded to the widget.
88
+ */
89
+ export function extAppToolResultsMatch(a: CallToolResult, b: CallToolResult): boolean {
90
+ if (a === b) return true;
91
+ if (a.isError !== b.isError) return false;
92
+ try {
93
+ const sa = JSON.stringify(a);
94
+ const sb = JSON.stringify(b);
95
+ if (sa === undefined || sb === undefined) return false;
96
+ if (Math.abs(sa.length - sb.length) > 2_000_000) return false;
97
+ return sa === sb;
98
+ } catch {
99
+ return false;
100
+ }
101
+ }