pi-background-tasks 0.3.0 → 0.4.0
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/PUBLISHING.md +6 -6
- package/README.md +17 -3
- package/TESTING.md +2 -1
- package/TEST_PLAN.md +2 -1
- package/package.json +1 -1
- package/src/core/common.ts +72 -0
- package/src/core/update-check.ts +99 -0
- package/src/extension.ts +61 -2
package/PUBLISHING.md
CHANGED
|
@@ -32,8 +32,8 @@ npm publish --access public
|
|
|
32
32
|
Pi install smoke after publish:
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
pi -e npm:pi-background-tasks@0.
|
|
36
|
-
pi install npm:pi-background-tasks@0.
|
|
35
|
+
pi -e npm:pi-background-tasks@0.4.0 --offline --no-tools --no-session -p "/jobs"
|
|
36
|
+
pi install npm:pi-background-tasks@0.4.0
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
## Publish to git
|
|
@@ -48,15 +48,15 @@ git commit -m "Release pi-background-tasks"
|
|
|
48
48
|
git branch -M main
|
|
49
49
|
git remote add origin git@github.com:ismailsaleekh/pi-background-tasks.git
|
|
50
50
|
git push -u origin main
|
|
51
|
-
git tag v0.
|
|
52
|
-
git push origin v0.
|
|
51
|
+
git tag v0.4.0
|
|
52
|
+
git push origin v0.4.0
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
Pi install smoke after git tag:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
pi -e git:github.com/ismailsaleekh/pi-background-tasks@v0.
|
|
59
|
-
pi install git:github.com/ismailsaleekh/pi-background-tasks@v0.
|
|
58
|
+
pi -e git:github.com/ismailsaleekh/pi-background-tasks@v0.4.0 --offline --no-tools --no-session -p "/jobs"
|
|
59
|
+
pi install git:github.com/ismailsaleekh/pi-background-tasks@v0.4.0
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
## pi.dev/packages
|
package/README.md
CHANGED
|
@@ -9,19 +9,19 @@ This package adds named, tracked background shell jobs with durable output files
|
|
|
9
9
|
From npm after publish:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
pi install npm:pi-background-tasks@0.
|
|
12
|
+
pi install npm:pi-background-tasks@0.4.0
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
From git after pushing this package to its standalone repository and tagging:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
pi install git:github.com/ismailsaleekh/pi-background-tasks@v0.
|
|
18
|
+
pi install git:github.com/ismailsaleekh/pi-background-tasks@v0.4.0
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
For project-local install:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
pi install -l npm:pi-background-tasks@0.
|
|
24
|
+
pi install -l npm:pi-background-tasks@0.4.0
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
## Commands
|
|
@@ -32,6 +32,7 @@ pi install -l npm:pi-background-tasks@0.3.0
|
|
|
32
32
|
- `/kill <id>` — stop a running task.
|
|
33
33
|
- `/tasks` or `/bg-tasks` — fallback command to open the task manager UI.
|
|
34
34
|
- `/bg-clear` — clear finished background-task footer notices.
|
|
35
|
+
- `/bg-update` — print update instructions when a newer published version exists (instruct-only; never self-installs).
|
|
35
36
|
|
|
36
37
|
## Footer dock UX
|
|
37
38
|
|
|
@@ -65,6 +66,19 @@ Dock controls:
|
|
|
65
66
|
| `R` | Rerun selected command |
|
|
66
67
|
| `c` | Show copyable output path |
|
|
67
68
|
| `x` / `Esc` / `q` | Close dock |
|
|
69
|
+
| `/bg-update` | Print update instructions when a newer version is published |
|
|
70
|
+
|
|
71
|
+
### Update-available notice
|
|
72
|
+
|
|
73
|
+
When a newer version of `pi-background-tasks` has been published to npm, the footer appends a compact, instruct-only segment after the entry hint:
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
bg 1 running · 1 failed · Shift↓ · /bg-clear · ⬆ v0.5.0 /bg-update
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
When no tasks are active, the notice still appears on its own (`bg ⬆ v0.5.0 /bg-update`) so the update hint is visible. The segment is rendered only when a strictly newer published version exists; `/bg-update` prints the npm and git update commands and never installs or self-updates.
|
|
80
|
+
|
|
81
|
+
The lookup runs at most once per session on `session_start`, is time-boxed, and is fully offline-safe: it never blocks or errors the footer/session, and on an offline or failed lookup it simply renders no segment (it never pins a misleading version). The check is skipped entirely when `PI_OFFLINE=1`, and can be disabled explicitly with `PI_BG_DISABLE_UPDATE_CHECK=1`. Set `PI_BG_REGISTRY_URL` to point the check at a registry mirror instead of `https://registry.npmjs.org`.
|
|
68
82
|
|
|
69
83
|
## LLM tools
|
|
70
84
|
|
package/TESTING.md
CHANGED
|
@@ -71,7 +71,8 @@ Tests must not use the user's real `~/.pi/agent`.
|
|
|
71
71
|
Implemented coverage includes:
|
|
72
72
|
|
|
73
73
|
- tools: `bg_run`, `bg_status`, `bg_logs`, `bg_kill`, including required `isAgent` schema/runtime validation, unknown/ambiguous IDs, completed-kill failure, legacy no-name preparation, head/tail truncation, and notification on/off behavior
|
|
74
|
-
- commands: `/bg`, `/jobs`, `/logs`, `/kill`, `/tasks`, `/bg-tasks`, `/bg-clear` discovery, happy paths, `/bg --agent` parsing, finished-notice clearing, malformed `/bg`, unknown/ambiguous IDs, completed-task `/kill`, byte-limit normalization, and RPC no-hang fallback behavior
|
|
74
|
+
- commands: `/bg`, `/jobs`, `/logs`, `/kill`, `/tasks`, `/bg-tasks`, `/bg-clear`, `/bg-update` discovery, happy paths, `/bg --agent` parsing, finished-notice clearing, malformed `/bg`, unknown/ambiguous IDs, completed-task `/kill`, byte-limit normalization, and RPC no-hang fallback behavior
|
|
75
|
+
- update-available notice: semver parse/compare/precedence, `formatUpdateSegment`, npm/`package.json` payload narrowing, and injected-fetch success/404/throw/timeout (unit); localhost-registry footer segment (idle + appended to an active footer), `/bg-update` non-installing instructions, and opt-out/offline/already-current/registry-failure no-segment-and-no-throw paths (SDK); `/bg-update` discovery and offline instructions (RPC). The check is one-shot on `session_start`, time-boxed, offline-safe, gated by `PI_OFFLINE`/`PI_BG_DISABLE_UPDATE_CHECK`, and `PI_BG_REGISTRY_URL` overrides the registry endpoint
|
|
75
76
|
- shortcut/UI: component coverage for focused dock list/detail/key handling, empty/history/unread states, paging, close aliases, stop/stop-all/rerun/path actions, missing output files; SDK coverage for explicit `/bg-clear` finished-notice clearing, `/bg-clear` footer hinting, optional `Ctrl+Alt+C` fallback shortcut registration, and mixed failed/stopped/done/focused footer status; RPC coverage that `/bg-clear` works as a terminal-independent clear path; and PTY coverage for `/tasks`, `/bg-tasks`, real `Shift+Down`, arrows, page keys, detail/back/history/stop/stop-all/rerun/path/close, failed unread badges, and running/completed/failed/killed rerun paths
|
|
76
77
|
- runtime files: output and metadata files under `.pi/tasks/`, persisted `isAgent` classification, task-owned context-window telemetry snapshots, cumulative background Pi-agent token usage, tool-use counts, agent model identifier (preferring the fully-qualified `provider/model` form), explicit `isAgent:true` telemetry wrapping for background `pi` agents, `isAgent:false` non-wrapping for scripts, real child `pi --mode json` tool-event parsing, split/large telemetry ingestion, metadata after completion/failure, local tarball install contents
|
|
77
78
|
- safety: kill, already-finished kill failure, timeout failure, spawn failure, low output-cap failure, multi-task shutdown cleanup, process-group kill fallback, Windows child-kill behavior, SIGKILL escalation, duplicate finalization/notification races, metadata/notification failure handling, and pruning
|
package/TEST_PLAN.md
CHANGED
|
@@ -12,7 +12,7 @@ This package follows:
|
|
|
12
12
|
|---|---|
|
|
13
13
|
| Package | `pi-background-tasks` |
|
|
14
14
|
| Extension entrypoint | `extensions/background-tasks.ts` |
|
|
15
|
-
| Public commands | `/bg`, `/jobs`, `/logs`, `/kill`, `/tasks`, `/bg-tasks`, `/bg-clear` |
|
|
15
|
+
| Public commands | `/bg`, `/jobs`, `/logs`, `/kill`, `/tasks`, `/bg-tasks`, `/bg-clear`, `/bg-update` |
|
|
16
16
|
| Public tools | `bg_run`, `bg_status`, `bg_logs`, `bg_kill` |
|
|
17
17
|
| Shortcuts | `Shift+Down`; optional fallback `Ctrl+Alt+C` |
|
|
18
18
|
| Custom UI | footer status + focused bottom dock overlay |
|
|
@@ -62,6 +62,7 @@ This package follows:
|
|
|
62
62
|
| Dock close | `x`/`Esc`/`q` | | | | yes | yes | | | Component + PTY. |
|
|
63
63
|
| Shortcut opens dock | `Shift+Down` | | registration | | | yes | | | PTY sends xterm `ESC [ 1 ; 2 B`. |
|
|
64
64
|
| Clear finished notices | `/bg-clear`, optional `Ctrl+Alt+C` fallback | | yes | yes | | | | | `/bg-clear` is the canonical terminal-independent path and is advertised in the footer. SDK invokes the slash-command handler and verifies fallback shortcut registration; RPC verifies `/bg-clear` clears finished notices; finished notices remain until explicit clear. |
|
|
65
|
+
| Update-available footer notice | `⬆ v<latest> /bg-update` footer segment + `/bg-update` command | yes | yes | yes | | | | | Unit covers semver parse/compare/precedence, `isNewerVersion`, `formatUpdateSegment`, npm/`package.json` payload narrowing, injected-fetch success/404/throw/timeout, and `package.json` read/degrade. SDK uses a localhost registry to verify the idle and append-to-active footer segment, `/bg-update` non-installing instructions, and that opt-out (`PI_BG_DISABLE_UPDATE_CHECK=1`), offline (`PI_OFFLINE=1`), already-current, and registry-failure paths render no segment and never throw. RPC verifies `/bg-update` discovery and offline non-installing instructions. The check is one-shot per `session_start`, time-boxed, offline-safe, and never runs on the status tick. |
|
|
65
66
|
| Runtime output files | `.pi/tasks/...output` | yes | yes | | | | | | SDK asserts existence. |
|
|
66
67
|
| Runtime metadata files | `.pi/tasks/...json` | yes | yes | | | | | | SDK asserts shape/status/name/context usage; registry unit tests cover metadata failure/update ordering. |
|
|
67
68
|
| Timeout kills task | `timeoutSeconds` | | yes | | | | | | SDK. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-background-tasks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Claude-Code-like named background shell task manager for Pi: bg_run tools, /bg commands, Shift+Down footer dock, bounded logs, kill/timeout safety, and completion wakeups.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "ISC",
|
package/src/core/common.ts
CHANGED
|
@@ -395,3 +395,75 @@ export async function boundedRead(
|
|
|
395
395
|
export function escapeXml(value: string): string {
|
|
396
396
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
397
397
|
}
|
|
398
|
+
|
|
399
|
+
export const UPDATE_COMMAND = "/bg-update";
|
|
400
|
+
|
|
401
|
+
type ParsedSemver = {
|
|
402
|
+
major: number;
|
|
403
|
+
minor: number;
|
|
404
|
+
patch: number;
|
|
405
|
+
prerelease: string[];
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const SEMVER_PATTERN = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
409
|
+
|
|
410
|
+
export function parseSemver(value: string): ParsedSemver | undefined {
|
|
411
|
+
if (typeof value !== "string") return undefined;
|
|
412
|
+
const match = value.trim().match(SEMVER_PATTERN);
|
|
413
|
+
if (!match) return undefined;
|
|
414
|
+
const major = Number(match[1]);
|
|
415
|
+
const minor = Number(match[2]);
|
|
416
|
+
const patch = Number(match[3]);
|
|
417
|
+
if (!Number.isInteger(major) || !Number.isInteger(minor) || !Number.isInteger(patch)) return undefined;
|
|
418
|
+
const prerelease = match[4] ? match[4].split(".") : [];
|
|
419
|
+
return { major, minor, patch, prerelease };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function comparePrerelease(a: string[], b: string[]): number {
|
|
423
|
+
if (a.length === 0 && b.length === 0) return 0;
|
|
424
|
+
// A version without prerelease identifiers outranks the same core with prerelease identifiers.
|
|
425
|
+
if (a.length === 0) return 1;
|
|
426
|
+
if (b.length === 0) return -1;
|
|
427
|
+
const shared = Math.min(a.length, b.length);
|
|
428
|
+
for (let i = 0; i < shared; i++) {
|
|
429
|
+
const idA = a[i];
|
|
430
|
+
const idB = b[i];
|
|
431
|
+
if (idA === undefined || idB === undefined) break;
|
|
432
|
+
if (idA === idB) continue;
|
|
433
|
+
const numericA = /^\d+$/.test(idA);
|
|
434
|
+
const numericB = /^\d+$/.test(idB);
|
|
435
|
+
if (numericA && numericB) {
|
|
436
|
+
const diff = Number(idA) - Number(idB);
|
|
437
|
+
if (diff !== 0) return diff < 0 ? -1 : 1;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
// Numeric identifiers always have lower precedence than non-numeric identifiers.
|
|
441
|
+
if (numericA) return -1;
|
|
442
|
+
if (numericB) return 1;
|
|
443
|
+
return idA < idB ? -1 : 1;
|
|
444
|
+
}
|
|
445
|
+
if (a.length === b.length) return 0;
|
|
446
|
+
return a.length < b.length ? -1 : 1;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Compare two semver strings. Returns -1/0/1, or undefined when either side is not valid semver. */
|
|
450
|
+
export function compareSemver(a: string, b: string): number | undefined {
|
|
451
|
+
const left = parseSemver(a);
|
|
452
|
+
const right = parseSemver(b);
|
|
453
|
+
if (!left || !right) return undefined;
|
|
454
|
+
if (left.major !== right.major) return left.major < right.major ? -1 : 1;
|
|
455
|
+
if (left.minor !== right.minor) return left.minor < right.minor ? -1 : 1;
|
|
456
|
+
if (left.patch !== right.patch) return left.patch < right.patch ? -1 : 1;
|
|
457
|
+
return comparePrerelease(left.prerelease, right.prerelease);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function isNewerVersion(latest: string, current: string): boolean {
|
|
461
|
+
return compareSemver(latest, current) === 1;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** Footer segment shown only when a newer published version exists; undefined otherwise. */
|
|
465
|
+
export function formatUpdateSegment(latest: string | undefined, current: string): string | undefined {
|
|
466
|
+
if (!latest) return undefined;
|
|
467
|
+
if (!isNewerVersion(latest, current)) return undefined;
|
|
468
|
+
return `\u2b06 v${latest} ${UPDATE_COMMAND}`;
|
|
469
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Typed, offline-safe boundary for the "update available" footer notice.
|
|
5
|
+
*
|
|
6
|
+
* All network access flows through an injectable {@link FetchLike} so unit tests
|
|
7
|
+
* never touch the real npm registry. Every failure path (offline, timeout, bad
|
|
8
|
+
* status, malformed payload) resolves to `undefined` and never throws, so the
|
|
9
|
+
* footer/session can never hang or error because of an update check.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
13
|
+
export const DEFAULT_UPDATE_TIMEOUT_MS = 2000;
|
|
14
|
+
|
|
15
|
+
export type FetchResponseLike = {
|
|
16
|
+
readonly ok: boolean;
|
|
17
|
+
readonly status: number;
|
|
18
|
+
json(): Promise<unknown>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type FetchLike = (url: string, init: { signal: AbortSignal }) => Promise<FetchResponseLike>;
|
|
22
|
+
|
|
23
|
+
export type FetchLatestVersionOptions = {
|
|
24
|
+
packageName: string;
|
|
25
|
+
registryUrl?: string;
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
fetchImpl?: FetchLike;
|
|
28
|
+
onError?: (error: Error) => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type PackageInfo = {
|
|
32
|
+
name?: string;
|
|
33
|
+
version?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
37
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
38
|
+
return value as Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readNonEmptyString(record: Record<string, unknown>, key: string): string | undefined {
|
|
42
|
+
const value = record[key];
|
|
43
|
+
if (typeof value !== "string") return undefined;
|
|
44
|
+
const trimmed = value.trim();
|
|
45
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Narrow an npm registry `<pkg>/latest` JSON payload to its `version` string. */
|
|
49
|
+
export function parseLatestVersionPayload(payload: unknown): string | undefined {
|
|
50
|
+
const record = asRecord(payload);
|
|
51
|
+
if (!record) return undefined;
|
|
52
|
+
return readNonEmptyString(record, "version");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Narrow a parsed `package.json` payload to the fields this extension needs. */
|
|
56
|
+
export function parsePackageInfo(payload: unknown): PackageInfo {
|
|
57
|
+
const record = asRecord(payload);
|
|
58
|
+
if (!record) return {};
|
|
59
|
+
const info: PackageInfo = {};
|
|
60
|
+
const name = readNonEmptyString(record, "name");
|
|
61
|
+
const version = readNonEmptyString(record, "version");
|
|
62
|
+
if (name !== undefined) info.name = name;
|
|
63
|
+
if (version !== undefined) info.version = version;
|
|
64
|
+
return info;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const realFetch: FetchLike = (url, init) => fetch(url, init);
|
|
68
|
+
|
|
69
|
+
/** Time-boxed, never-throwing lookup of the latest published version of a package. */
|
|
70
|
+
export async function fetchLatestVersion(options: FetchLatestVersionOptions): Promise<string | undefined> {
|
|
71
|
+
const registryUrl = (options.registryUrl ?? DEFAULT_NPM_REGISTRY_URL).replace(/\/+$/, "");
|
|
72
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_UPDATE_TIMEOUT_MS;
|
|
73
|
+
const doFetch = options.fetchImpl ?? realFetch;
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
76
|
+
try {
|
|
77
|
+
const url = `${registryUrl}/${encodeURIComponent(options.packageName)}/latest`;
|
|
78
|
+
const response = await doFetch(url, { signal: controller.signal });
|
|
79
|
+
if (!response.ok) return undefined;
|
|
80
|
+
const payload = await response.json();
|
|
81
|
+
return parseLatestVersionPayload(payload);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
options.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
84
|
+
return undefined;
|
|
85
|
+
} finally {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Read `name`/`version` from a local `package.json`, returning `{}` on every failure. */
|
|
91
|
+
export function readPackageInfo(packageJsonUrl: URL | string, onError?: (error: Error) => void): PackageInfo {
|
|
92
|
+
try {
|
|
93
|
+
const parsed: unknown = JSON.parse(readFileSync(packageJsonUrl, "utf8"));
|
|
94
|
+
return parsePackageInfo(parsed);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
}
|
package/src/extension.ts
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
MAX_LOG_BYTES,
|
|
8
8
|
deriveTaskNameFromCommand,
|
|
9
9
|
formatSnapshotList,
|
|
10
|
+
formatUpdateSegment,
|
|
11
|
+
isNewerVersion,
|
|
10
12
|
normalizeMaxBytes,
|
|
11
13
|
normalizeTaskName,
|
|
12
14
|
parseBgCommandArgs,
|
|
@@ -20,6 +22,7 @@ import {
|
|
|
20
22
|
type BgTaskSnapshot,
|
|
21
23
|
type StartTaskOptions,
|
|
22
24
|
} from "./core/common.js";
|
|
25
|
+
import { fetchLatestVersion, readPackageInfo, type FetchLatestVersionOptions } from "./core/update-check.js";
|
|
23
26
|
import { BackgroundTaskRegistry } from "./core/registry.js";
|
|
24
27
|
import { BackgroundTasksManager, type BackgroundTaskForUi, type TaskManagerResult } from "./ui/background-tasks-manager.js";
|
|
25
28
|
|
|
@@ -35,6 +38,13 @@ import { BackgroundTasksManager, type BackgroundTaskForUi, type TaskManagerResul
|
|
|
35
38
|
|
|
36
39
|
const STATUS_INTERVAL_MS = 1000;
|
|
37
40
|
const COMMAND_PREVIEW_CHARS = 90;
|
|
41
|
+
const GIT_INSTALL_TARGET = "git:github.com/ismailsaleekh/pi-background-tasks";
|
|
42
|
+
|
|
43
|
+
const packageInfo = readPackageInfo(new URL("../package.json", import.meta.url), (error) => {
|
|
44
|
+
console.error(`[background-tasks] failed to read package version: ${error.message}`);
|
|
45
|
+
});
|
|
46
|
+
const PACKAGE_NAME = packageInfo.name ?? "pi-background-tasks";
|
|
47
|
+
const PACKAGE_VERSION = packageInfo.version;
|
|
38
48
|
const LIGHT_BLUE_BG = "\x1b[48;2;183;223;255m";
|
|
39
49
|
const LIGHT_BLUE_FG = "\x1b[38;2;11;70;110m";
|
|
40
50
|
const ANSI_RESET = "\x1b[0m";
|
|
@@ -88,6 +98,8 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
|
|
|
88
98
|
let currentCtx: ExtensionContext | undefined;
|
|
89
99
|
let dockOpen = false;
|
|
90
100
|
let statusInterval: NodeJS.Timeout | undefined;
|
|
101
|
+
let latestKnownVersion: string | undefined;
|
|
102
|
+
let updateCheckStarted = false;
|
|
91
103
|
|
|
92
104
|
const registry = new BackgroundTaskRegistry({
|
|
93
105
|
onChange: () => updateUi(),
|
|
@@ -129,9 +141,10 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
|
|
|
129
141
|
const unseenStopped = allTasks.filter((task) => task.status === "killed" && !seenTaskIds.has(task.id));
|
|
130
142
|
const unseenDone = allTasks.filter((task) => task.status === "completed" && !seenTaskIds.has(task.id));
|
|
131
143
|
const unseenFinishedCount = unseenFailed.length + unseenStopped.length + unseenDone.length;
|
|
144
|
+
const updateSegment = formatUpdateSegment(latestKnownVersion, PACKAGE_VERSION ?? "");
|
|
132
145
|
ctx.ui.setWidget("background-tasks", undefined);
|
|
133
146
|
if (running.length === 0 && unseenFinishedCount === 0) {
|
|
134
|
-
ctx.ui.setStatus("background-tasks", undefined);
|
|
147
|
+
ctx.ui.setStatus("background-tasks", updateSegment ? lightBlue(` bg ${updateSegment} `) : undefined);
|
|
135
148
|
return;
|
|
136
149
|
}
|
|
137
150
|
|
|
@@ -141,7 +154,9 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
|
|
|
141
154
|
if (unseenStopped.length > 0) parts.push(`${unseenStopped.length} stopped`);
|
|
142
155
|
if (unseenDone.length > 0) parts.push(`${unseenDone.length} done`);
|
|
143
156
|
const entryHint = dockOpen ? "focused" : `Shift↓${unseenFinishedCount > 0 ? " · /bg-clear" : ""}`;
|
|
144
|
-
const
|
|
157
|
+
const segments = [...parts, entryHint];
|
|
158
|
+
if (updateSegment) segments.push(updateSegment);
|
|
159
|
+
const label = ` bg ${segments.join(" · ")} `;
|
|
145
160
|
ctx.ui.setStatus("background-tasks", lightBlue(label));
|
|
146
161
|
} catch (error) {
|
|
147
162
|
console.error(`[background-tasks] UI update failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -227,6 +242,26 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
|
|
|
227
242
|
return new Text(`${theme.fg(color, `[bg ${status}]`)} ${theme.fg("accent", name)} ${theme.fg("dim", `(${id})`)}${output}${error}`, 0, 0);
|
|
228
243
|
});
|
|
229
244
|
|
|
245
|
+
async function scheduleUpdateCheck(ctx: ExtensionContext): Promise<void> {
|
|
246
|
+
if (updateCheckStarted) return;
|
|
247
|
+
updateCheckStarted = true;
|
|
248
|
+
const env = process.env;
|
|
249
|
+
if (env["PI_BG_DISABLE_UPDATE_CHECK"] === "1") return;
|
|
250
|
+
if (env["PI_OFFLINE"] === "1") return;
|
|
251
|
+
if (!PACKAGE_VERSION) return;
|
|
252
|
+
const options: FetchLatestVersionOptions = {
|
|
253
|
+
packageName: PACKAGE_NAME,
|
|
254
|
+
onError: (error) => console.error(`[background-tasks] update check skipped: ${error.message}`),
|
|
255
|
+
};
|
|
256
|
+
const registryUrl = env["PI_BG_REGISTRY_URL"];
|
|
257
|
+
if (registryUrl) options.registryUrl = registryUrl;
|
|
258
|
+
const latest = await fetchLatestVersion(options);
|
|
259
|
+
if (latest && isNewerVersion(latest, PACKAGE_VERSION)) {
|
|
260
|
+
latestKnownVersion = latest;
|
|
261
|
+
updateUi(ctx);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
230
265
|
pi.on("session_start", async (_event, ctx) => {
|
|
231
266
|
registry.setShuttingDown(false);
|
|
232
267
|
currentCtx = ctx;
|
|
@@ -234,6 +269,8 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
|
|
|
234
269
|
updateUi(ctx);
|
|
235
270
|
if (statusInterval) clearInterval(statusInterval);
|
|
236
271
|
statusInterval = setInterval(() => updateUi(), STATUS_INTERVAL_MS);
|
|
272
|
+
// One-shot, non-blocking: never awaited on the session-start path or the status tick.
|
|
273
|
+
void scheduleUpdateCheck(ctx);
|
|
237
274
|
});
|
|
238
275
|
|
|
239
276
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
@@ -301,6 +338,28 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
|
|
|
301
338
|
},
|
|
302
339
|
});
|
|
303
340
|
|
|
341
|
+
pi.registerCommand("bg-update", {
|
|
342
|
+
description: "Show how to update pi-background-tasks to the latest published version",
|
|
343
|
+
handler: async (_args, ctx) => {
|
|
344
|
+
const current = PACKAGE_VERSION ?? "unknown";
|
|
345
|
+
const latest = latestKnownVersion;
|
|
346
|
+
const pinnedNpm = latest ? `${PACKAGE_NAME}@${latest}` : `${PACKAGE_NAME}@<version>`;
|
|
347
|
+
const pinnedGit = latest ? `${GIT_INSTALL_TARGET}@v${latest}` : `${GIT_INSTALL_TARGET}@<tag>`;
|
|
348
|
+
const lines = [
|
|
349
|
+
latest
|
|
350
|
+
? `pi-background-tasks ${current} is installed; ${latest} is the latest published version.`
|
|
351
|
+
: `pi-background-tasks ${current} is installed.`,
|
|
352
|
+
"Update from npm:",
|
|
353
|
+
` pi install npm:${PACKAGE_NAME}@latest`,
|
|
354
|
+
` pi install npm:${pinnedNpm}`,
|
|
355
|
+
"Or update from git tags:",
|
|
356
|
+
` pi install ${pinnedGit}`,
|
|
357
|
+
"This command only prints update instructions; it does not install or self-update.",
|
|
358
|
+
];
|
|
359
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
|
|
304
363
|
pi.registerShortcut("shift+down" satisfies KeyId, {
|
|
305
364
|
description: "Open focused background task footer dock",
|
|
306
365
|
handler: async (ctx) => {
|