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 +25 -0
- package/package.json +2 -1
- package/src/logs.ts +6 -0
- package/src/navigator-provider.ts +10 -3
- package/src/process.ts +25 -3
- package/src/registry.ts +5 -1
- package/src/remote-task-preset.ts +631 -0
- package/src/runtime.ts +483 -55
- package/src/tools.ts +53 -18
- package/src/types.ts +43 -0
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
|
|
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
|
-
|
|
60
|
-
return { action: "stopped", providerId: "background-tasks", id, 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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|