pi-better-background-tasks 0.1.18 → 0.2.1

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
@@ -18,6 +18,31 @@ Use `pi-better-background-tasks` when a command should keep running while the fo
18
18
  - Show active work in Pi's background-work navigator.
19
19
  - Flag running tasks with no observable output or completed poll as stalled.
20
20
 
21
+ ## Remote SSH
22
+
23
+ Prefer structured `ssh` fields over hand-written `ssh` command lines. A remote
24
+ spawn uses a durable tmux session by default, while a remote watch opens one
25
+ direct SSH poll per interval and does not require tmux. The package keeps the
26
+ same local metadata, logs, terminal statuses, callbacks, and `/reload` recovery
27
+ for both.
28
+
29
+ ```json
30
+ {
31
+ "name": "remote build",
32
+ "command": "npm run build",
33
+ "ssh": { "host": "build.example", "user": "deploy" },
34
+ "remote": { "workdir": "/srv/app" },
35
+ "timeout_seconds": 1800
36
+ }
37
+ ```
38
+
39
+ Tmux-backed spawn can install tmux non-interactively when the remote host allows
40
+ it and fails closed with copy-pasteable setup guidance when it cannot. Set
41
+ `remote.session=direct` only as an explicit escape hatch for short jobs: stop or
42
+ timeout can terminate the local SSH client but the remote process may still be
43
+ running. See the detailed usage notes for bootstrap policy, watch conditions,
44
+ timeouts, and v1 non-goals.
45
+
21
46
  ## Install
22
47
 
23
48
  ```sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-background-tasks",
3
- "version": "0.1.18",
3
+ "version": "0.2.1",
4
4
  "description": "Pi extension for durable background shell tasks, watchers, logs, and status inspection.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -43,6 +43,7 @@
43
43
  "files": [
44
44
  "src/**/*.ts",
45
45
  "!src/**/*.test.ts",
46
+ "!src/test-support/**",
46
47
  "README.md"
47
48
  ],
48
49
  "peerDependencies": {
package/src/logs.ts CHANGED
@@ -21,6 +21,12 @@ export function appendLine(logPath: string, line: string): void {
21
21
  appendFileSync(logPath, `${line}\n`);
22
22
  }
23
23
 
24
+ export function appendTaskOutput(logPath: string, output: string): void {
25
+ if (!output) return;
26
+ mkdirSync(dirname(logPath), { recursive: true });
27
+ appendFileSync(logPath, output);
28
+ }
29
+
24
30
  export function readLog(logPath: string, tailLines?: number): { text: string; truncated: boolean } {
25
31
  const requestedRows = tailLines && tailLines > 0 ? Math.floor(tailLines) : undefined;
26
32
  const tail = readBoundedTail(logPath, MAX_LOG_TAIL_READ_BYTES);
@@ -56,8 +56,8 @@ const provider: BackgroundWorkProvider = {
56
56
  const meta = readMeta(id);
57
57
  if (!meta) return { action: "missing", providerId: "background-tasks", id };
58
58
  if (meta.status === "running") {
59
- const stopped = stopTask(piRef as ExtensionAPI, id);
60
- return { action: "stopped", providerId: "background-tasks", id, status: stopped?.status };
59
+ void stopTask(piRef as ExtensionAPI, id);
60
+ return { action: "stopped", providerId: "background-tasks", id, status: "stopping" };
61
61
  }
62
62
  meta.dismissedAt = Date.now();
63
63
  writeMeta(meta);
@@ -104,7 +104,7 @@ function rowFromMeta(meta: BackgroundTaskMeta, now: number): BackgroundWorkRow {
104
104
  return {
105
105
  providerId: "background-tasks",
106
106
  id: meta.id,
107
- name: meta.name,
107
+ name: meta.name ?? (meta.ssh ? `${meta.ssh.target}${meta.remote?.session ? ` ${meta.remote.session}` : ""}` : undefined),
108
108
  status: meta.status,
109
109
  statusTone: toneForStatus(meta.status),
110
110
  kind: meta.kind === "command_watch" ? "watch" : "process",
@@ -134,6 +134,11 @@ function detailFromMeta(meta: BackgroundTaskMeta | undefined, now: number, optio
134
134
  { label: "pgid", value: meta.pgid != null ? String(meta.pgid) : "-" },
135
135
  { label: "log", value: meta.logPath },
136
136
  ];
137
+ if (meta.ssh) metadata.push({ label: "remote", value: meta.ssh.target });
138
+ if (meta.remote?.session) metadata.push({ label: "remote mode", value: meta.remote.session });
139
+ if (meta.remote?.sessionName) metadata.push({ label: "remote session", value: meta.remote.sessionName });
140
+ if (meta.remote?.bootstrapMessage) metadata.push({ label: "remote setup", value: meta.remote.bootstrapMessage });
141
+ if (meta.remote?.warning) metadata.push({ label: "warning", value: meta.remote.warning });
137
142
  if (meta.deadlineAt) metadata.push({ label: "deadline", value: formatDuration(meta.deadlineAt - now) });
138
143
  if (meta.lastCheckedAt) metadata.push({ label: "checked", value: `${formatDuration(now - meta.lastCheckedAt)} ago` });
139
144
  if (meta.status === "running") {
@@ -187,6 +192,7 @@ function commandLabel(meta: BackgroundTaskMeta): string {
187
192
  }
188
193
 
189
194
  function compactCommandLabel(meta: BackgroundTaskMeta): string {
195
+ if (meta.ssh) return `${meta.ssh.target}${meta.remote?.session ? ` ${meta.remote.session}` : ""}`;
190
196
  const value = commandLabel(meta).trim();
191
197
  const parts = value.split(/\s+/).filter(Boolean);
192
198
  if (parts.length <= 3) return value;
@@ -196,6 +202,7 @@ function compactCommandLabel(meta: BackgroundTaskMeta): string {
196
202
  function secondaryLabel(meta: BackgroundTaskMeta): string | undefined {
197
203
  const parts = [];
198
204
  if (meta.cwd) parts.push(meta.cwd);
205
+ if (meta.remote?.session) parts.push(meta.remote.session);
199
206
  if (meta.pid != null) parts.push(`pid ${meta.pid}`);
200
207
  if (meta.lastExitCode !== undefined) parts.push(`exit ${meta.lastExitCode}`);
201
208
  return parts.length ? parts.join(" · ") : undefined;
package/src/process.ts CHANGED
@@ -36,12 +36,17 @@ export function spawnCommand(spec: CommandSpec, logPath: string, detached: boole
36
36
  return { child, pgid: detached && child.pid ? child.pid : undefined };
37
37
  }
38
38
 
39
- export function runCommandOnce(spec: CommandSpec, maxBufferBytes = 1024 * 1024): Promise<CommandResult> {
39
+ export function runCommandOnce(
40
+ spec: CommandSpec,
41
+ maxBufferBytes = 1024 * 1024,
42
+ timeoutMs?: number,
43
+ ): Promise<CommandResult> {
40
44
  validateCommandSpec(spec);
41
45
  const startedAt = Date.now();
42
46
  const child = spawnArgs(spec, false, ["ignore", "pipe", "pipe"]);
43
47
  let stdout = "";
44
48
  let stderr = "";
49
+ let timedOut = false;
45
50
  child.stdout?.on("data", (chunk: Buffer) => {
46
51
  if (Buffer.byteLength(stdout) < maxBufferBytes) stdout += chunk.toString("utf8");
47
52
  });
@@ -49,9 +54,26 @@ export function runCommandOnce(spec: CommandSpec, maxBufferBytes = 1024 * 1024):
49
54
  if (Buffer.byteLength(stderr) < maxBufferBytes) stderr += chunk.toString("utf8");
50
55
  });
51
56
  return new Promise((resolve, reject) => {
52
- child.on("error", reject);
57
+ const timeout = timeoutMs === undefined ? undefined : setTimeout(() => {
58
+ timedOut = true;
59
+ child.kill("SIGTERM");
60
+ }, Math.max(1, timeoutMs));
61
+ timeout?.unref();
62
+ child.on("error", (error) => {
63
+ if (timeout) clearTimeout(timeout);
64
+ reject(error);
65
+ });
53
66
  child.on("close", (exitCode, signal) => {
54
- resolve({ exitCode, signal, stdout, stderr, startedAt, endedAt: Date.now() });
67
+ if (timeout) clearTimeout(timeout);
68
+ resolve({
69
+ exitCode,
70
+ signal,
71
+ stdout,
72
+ stderr,
73
+ startedAt,
74
+ endedAt: Date.now(),
75
+ ...(timedOut ? { timedOut: true } : {}),
76
+ });
55
77
  });
56
78
  });
57
79
  }
package/src/registry.ts CHANGED
@@ -9,7 +9,11 @@ const metaCache = new Map<string, BackgroundTaskMeta>();
9
9
  const metaChangedListeners = new Set<() => void>();
10
10
 
11
11
  export function baseDir(): string {
12
- return join(tmpdir(), "pi-better-background-tasks");
12
+ const vitestPoolId = process.env.VITEST_POOL_ID;
13
+ const directory = vitestPoolId && /^\d+$/.test(vitestPoolId)
14
+ ? `pi-better-background-tasks-vitest-${vitestPoolId}`
15
+ : "pi-better-background-tasks";
16
+ return join(tmpdir(), directory);
13
17
  }
14
18
 
15
19
  export function tasksDir(): string {