@pify/shell-background 0.1.0 → 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.
package/README.md CHANGED
@@ -56,9 +56,13 @@ Put these in `.pi/shell-background.json` (project) or `<agentDir>/shell-backgrou
56
56
 
57
57
  `autoBackgroundMs` is how long a foreground command may run before it auto-backgrounds; set it to `0` to disable auto-background (explicit `background: true` still works). `PIFY_SHELL_BG_MS` overrides it for one run or in CI. `tailBytes` bounds how much of a job's log a status result shows. Bad values fall back to the defaults with a warning rather than taking the tool down.
58
58
 
59
- ## Coexistence
59
+ ## Coexistence with @pify/pretty
60
60
 
61
- This package owns the `bash` tool's execution. If you also run another extension that re-registers `bash` (a renderer like `@pify/pretty`, say), whichever loads last wins install order decides. `@pify/pretty` only changes rendering and leaves execution alone, so the usual advice is to let this package load after it.
61
+ Both this package and `@pify/pretty` re-register `bash`: this one to change its *execution* (async), pretty to change its *rendering* (compact, syntax-highlit). pi's `registerTool` is last-write-wins and gives an extension no way to read or wrap another's registered tool, so the two cannot be merged whichever loads last wins the whole `bash` tool.
62
+
63
+ **Load `@pify/shell-background` after `@pify/pretty`.** Async bash is the reason to install this package, so it should own execution; pretty keeps rendering every other tool (`read`, `edit`, `grep`, `write`, `ls`, `find`) — only its `bash`-specific rendering yields, and you still get pi's default bash view here. If pretty loads last instead, *this package's async execution is lost* and bash reverts to blocking — the outcome to avoid.
64
+
65
+ (A future pi API to compose registered tools would let both apply at once; today none exists.)
62
66
 
63
67
  ## License
64
68
 
@@ -47,6 +47,7 @@ import { DEFAULT_SETTINGS, resolveSettings, type ShellBgSettings } from "../src/
47
47
  import { backgroundedResult, deliveryMessage, DELIVERY_TYPE } from "../src/pending.ts";
48
48
  import { formatResult, formatList, header } from "../src/format.ts";
49
49
  import { buildWidgetLines } from "../src/widget.ts";
50
+ import { isFinished } from "../src/types.ts";
50
51
  import type { Job } from "../src/types.ts";
51
52
 
52
53
  type UiContext = ExtensionContext;
@@ -236,6 +237,19 @@ export default function shellBackground(pi: ExtensionAPI) {
236
237
 
237
238
  if (outcome === "auto") {
238
239
  scheduleDelivery(job, settle);
240
+ // A timeout the caller set still applies once the command is in the
241
+ // background: schedule the kill for the time it has left so the deadline
242
+ // the model expects is honoured rather than silently dropped. This timer
243
+ // deliberately outlives the `finally` below, so it is not in `timers`.
244
+ if (params.timeout && params.timeout > 0) {
245
+ const remaining = params.timeout * 1000 - (Date.now() - job.startedAt);
246
+ const killAt = setTimeout(() => {
247
+ if (job.status !== "running") return;
248
+ job.killedByUs = true;
249
+ killTree(job.pid);
250
+ }, Math.max(0, remaining));
251
+ killAt.unref?.();
252
+ }
239
253
  const r = backgroundedResult({
240
254
  id: job.id,
241
255
  command,
@@ -314,8 +328,13 @@ export default function shellBackground(pi: ExtensionAPI) {
314
328
  const known = registry.all().map((j) => j.id).join(", ") || "(none)";
315
329
  return { content: [{ type: "text", text: `No job "${id}". Known: ${known}` }], details: {}, isError: true };
316
330
  }
317
- // Reading marks it collected so it will not also be delivered unasked.
318
- job.delivered = true;
331
+ // Reading a finished job marks it collected so it will not also be
332
+ // delivered unasked. A still-running poll must never do this: setting
333
+ // delivered here would permanently cancel the promised auto-delivery.
334
+ if (isFinished(job)) {
335
+ job.delivered = true;
336
+ registry.persist(job);
337
+ }
319
338
  return {
320
339
  content: [{ type: "text", text: formatResult(job, settings.tailBytes) }],
321
340
  details: { id: job.id, status: job.status, exitCode: job.exitCode },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pify/shell-background",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Long-running bash goes async: background: true launches detached, and any command still running after 30s auto-backgrounds and delivers its result when it finishes",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/spawn.ts CHANGED
@@ -41,6 +41,11 @@ export function spawnToFile(
41
41
  ): Spawned {
42
42
  // Append so a re-attach or racing read never clips output already written.
43
43
  const out = createWriteStream(logPath, { flags: "a" });
44
+ // A write stream with no 'error' listener turns any disk error (ENOSPC,
45
+ // EACCES) or a stray write-after-end into an uncaught exception that takes the
46
+ // whole pi host down. Losing a log line is survivable; crashing the host is
47
+ // not — so swallow it here.
48
+ out.on("error", () => {});
44
49
 
45
50
  let child;
46
51
  try {