@gavin-hu/my-pi-agent 0.14.0 → 0.15.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/README.md +55 -33
- package/README.zh-CN.md +44 -27
- package/extensions/bytefly/README.md +193 -140
- package/extensions/bytefly/block/component.ts +7 -15
- package/extensions/bytefly/block/register.ts +6 -1
- package/extensions/bytefly/doctor/register.ts +24 -5
- package/extensions/bytefly/doctor/tools.ts +4 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +6 -1
- package/extensions/bytefly/footer/config.ts +10 -6
- package/extensions/bytefly/footer/price/price.ts +5 -3
- package/extensions/bytefly/footer/price/rates.ts +63 -26
- package/extensions/bytefly/footer/render/status.ts +3 -3
- package/extensions/bytefly/header/component.ts +3 -2
- package/extensions/bytefly/index.ts +3 -3
- package/extensions/bytefly/notice/component.ts +8 -3
- package/extensions/bytefly/notify/config.ts +6 -11
- package/extensions/bytefly/notify/dispatch.ts +13 -5
- package/extensions/bytefly/panel/commands.ts +21 -1
- package/extensions/bytefly/panel/format.ts +5 -2
- package/extensions/bytefly/panel/settings-runtime.ts +26 -6
- package/extensions/bytefly/panel/settings.ts +41 -4
- package/extensions/bytefly/panel/themes.ts +32 -0
- package/extensions/bytefly/panel/tui/editing.ts +77 -0
- package/extensions/bytefly/panel/tui/entries.ts +24 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +13 -8
- package/extensions/bytefly/panel/tui/frame.ts +121 -4
- package/extensions/bytefly/panel/tui/settings-view.ts +54 -15
- package/extensions/bytefly/panel/tui.ts +94 -152
- package/extensions/bytefly/picker/discovery.ts +1 -0
- package/extensions/bytefly/picker/types.ts +2 -0
- package/extensions/bytefly/separator/component.ts +14 -4
- package/extensions/bytefly/update/cache.ts +58 -19
- package/extensions/bytefly/update/check.ts +35 -0
- package/extensions/bytefly/update/format.ts +76 -35
- package/extensions/bytefly/update/register.ts +144 -55
- package/extensions/bytefly/update/state.ts +4 -2
- package/extensions/bytefly/update/suppress.ts +66 -0
- package/extensions/bytefly/update/types.ts +15 -0
- package/extensions/check/README.md +253 -0
- package/extensions/check/commands.ts +128 -0
- package/extensions/check/config.ts +39 -0
- package/extensions/check/detect.ts +269 -0
- package/extensions/check/failures.ts +230 -0
- package/extensions/check/format.ts +135 -0
- package/extensions/check/index.ts +26 -0
- package/extensions/check/render.ts +185 -0
- package/extensions/check/runner.ts +176 -0
- package/extensions/check/runtime.ts +244 -0
- package/extensions/check/source.ts +80 -0
- package/extensions/check/tools.ts +173 -0
- package/extensions/check/tui.ts +195 -0
- package/extensions/check/types.ts +99 -0
- package/extensions/document/README.md +1 -1
- package/extensions/document/extract/pdf.ts +8 -16
- package/extensions/document/formats.ts +2 -2
- package/extensions/file-browse/README.md +66 -32
- package/extensions/file-browse/client.ts +95 -32
- package/extensions/file-browse/config.ts +9 -9
- package/extensions/file-browse/git.ts +37 -47
- package/extensions/file-browse/html.ts +39 -58
- package/extensions/file-browse/markdown.ts +110 -92
- package/extensions/file-browse/raw-html.ts +478 -0
- package/extensions/file-browse/router.ts +32 -70
- package/extensions/file-browse/server.ts +18 -56
- package/extensions/file-browse/urls.ts +80 -0
- package/extensions/goal/README.md +103 -47
- package/extensions/goal/clarify-parse.ts +4 -9
- package/extensions/goal/clarify.ts +27 -4
- package/extensions/goal/commands.ts +24 -11
- package/extensions/goal/format.ts +41 -10
- package/extensions/goal/index.ts +39 -16
- package/extensions/goal/plan-route.ts +51 -0
- package/extensions/goal/schema.ts +6 -23
- package/extensions/goal/state.ts +39 -57
- package/extensions/goal/tools.ts +26 -20
- package/extensions/goal/types.ts +11 -1
- package/extensions/job/README.md +20 -6
- package/extensions/job/format.ts +18 -68
- package/extensions/job/index.ts +26 -41
- package/extensions/job/log-writer.ts +208 -0
- package/extensions/job/logs.ts +3 -2
- package/extensions/job/process.ts +6 -99
- package/extensions/job/runtime.ts +42 -181
- package/extensions/job/schema.ts +1 -4
- package/extensions/job/tools.ts +1 -15
- package/extensions/job/tui.ts +117 -307
- package/extensions/job/ui.ts +7 -33
- package/extensions/memory/README.md +1 -1
- package/extensions/memory/errors.ts +5 -6
- package/extensions/memory/tui.ts +14 -24
- package/extensions/plan/README.md +94 -18
- package/extensions/plan/commands.ts +9 -5
- package/extensions/plan/format.ts +87 -0
- package/extensions/plan/index.ts +31 -15
- package/extensions/plan/plans.ts +2 -3
- package/extensions/plan/policy.ts +0 -6
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/steps.ts +37 -8
- package/extensions/plan/todos.ts +44 -3
- package/extensions/plan/tools/enter.ts +94 -0
- package/extensions/plan/tools/exit.ts +296 -0
- package/extensions/plan/tools/nested.ts +21 -0
- package/extensions/plan/tools/steps.ts +136 -0
- package/extensions/plan/tools/todo-seed.ts +76 -0
- package/extensions/plan/tools/write.ts +127 -0
- package/extensions/plan/tools.ts +18 -703
- package/extensions/plan/web-client.ts +6 -22
- package/extensions/plan/web-page.ts +19 -30
- package/extensions/plan/web-router.ts +34 -71
- package/extensions/plan/web.ts +13 -40
- package/extensions/question/answers.ts +3 -8
- package/extensions/question/tui.ts +6 -6
- package/extensions/rewind/README.md +95 -22
- package/extensions/rewind/checklist.ts +185 -0
- package/extensions/rewind/commands.ts +8 -0
- package/extensions/rewind/config.ts +3 -11
- package/extensions/rewind/format.ts +220 -2
- package/extensions/rewind/git.ts +74 -7
- package/extensions/rewind/hunk-picker.ts +114 -0
- package/extensions/rewind/patch.ts +88 -0
- package/extensions/rewind/path-picker.ts +67 -100
- package/extensions/rewind/paths.ts +93 -19
- package/extensions/rewind/restore.ts +53 -10
- package/extensions/rewind/review-tui.ts +320 -0
- package/extensions/rewind/review.ts +397 -0
- package/extensions/rewind/rewind-tui.ts +4 -3
- package/extensions/rewind/runtime.ts +94 -17
- package/extensions/subagent/README.md +61 -6
- package/extensions/subagent/background-tui.ts +208 -0
- package/extensions/subagent/background.ts +470 -0
- package/extensions/subagent/commands.ts +80 -0
- package/extensions/subagent/config.ts +4 -0
- package/extensions/subagent/format.ts +24 -1
- package/extensions/subagent/index.ts +91 -2
- package/extensions/subagent/render.ts +13 -0
- package/extensions/subagent/run.ts +17 -16
- package/extensions/subagent/schema.ts +12 -0
- package/extensions/subagent/types.ts +11 -0
- package/extensions/subagent/ui.ts +50 -0
- package/extensions/todo/README.md +18 -13
- package/extensions/todo/format.ts +3 -2
- package/extensions/todo/runtime.ts +3 -2
- package/extensions/todo/schema.ts +8 -84
- package/extensions/todo/state.ts +5 -46
- package/extensions/todo/tools.ts +2 -1
- package/extensions/todo/types.ts +6 -15
- package/extensions/watch/README.md +14 -6
- package/extensions/watch/child.ts +150 -0
- package/extensions/watch/format.ts +8 -48
- package/extensions/watch/index.ts +42 -58
- package/extensions/watch/runtime.ts +49 -128
- package/extensions/watch/schema.ts +1 -4
- package/extensions/watch/tools.ts +1 -11
- package/extensions/watch/tui.ts +103 -273
- package/extensions/watch/ui.ts +5 -32
- package/extensions/web-access/README.md +1 -1
- package/extensions/web-access/fetch/page.ts +1 -1
- package/extensions/web-access/fetch/tool.ts +6 -2
- package/extensions/web-access/search/search.ts +2 -5
- package/extensions/wechat/README.md +1 -1
- package/extensions/wechat/client.ts +23 -0
- package/extensions/wechat/commands.ts +57 -42
- package/extensions/wechat/deliver.ts +5 -9
- package/extensions/wechat/errors.ts +3 -6
- package/extensions/wechat/inbound.ts +2 -5
- package/extensions/wechat/index.ts +2 -8
- package/extensions/wechat/outbound.ts +2 -4
- package/extensions/wechat/peer-worker.ts +3 -8
- package/extensions/wechat/poll.ts +10 -9
- package/extensions/wechat/presence.ts +35 -62
- package/extensions/wechat/reply-pipeline.ts +3 -1
- package/extensions/wechat/status-screen.ts +1 -1
- package/extensions/wechat/timer.ts +57 -0
- package/extensions/worktree/config.ts +8 -20
- package/extensions/worktree/enter.ts +7 -12
- package/extensions/worktree/git.ts +38 -69
- package/extensions/worktree/index.ts +5 -4
- package/extensions/worktree/registry.ts +7 -7
- package/extensions/worktree/root-tools.ts +2 -2
- package/extensions/worktree/status.ts +3 -2
- package/lib/README.md +22 -8
- package/lib/config.ts +50 -3
- package/lib/errors.ts +12 -0
- package/lib/format.ts +50 -0
- package/lib/git/read.ts +8 -0
- package/lib/goal-state.ts +23 -8
- package/lib/http/headers.ts +54 -0
- package/lib/http/index.ts +13 -0
- package/lib/http/response.ts +46 -0
- package/lib/http/router.ts +52 -0
- package/lib/http/server.ts +91 -0
- package/lib/lines.ts +77 -0
- package/lib/pdf.ts +82 -0
- package/lib/plan-entry.ts +71 -0
- package/lib/plan-state.ts +34 -0
- package/lib/report.ts +99 -0
- package/lib/requirements.ts +25 -19
- package/lib/spawn.ts +170 -0
- package/lib/todo-state.ts +110 -23
- package/lib/tool-names.ts +7 -0
- package/lib/tool-result.ts +33 -0
- package/lib/tui/README.md +68 -3
- package/lib/tui/chrome.ts +28 -4
- package/lib/tui/columns.ts +6 -1
- package/lib/tui/confirm.ts +65 -0
- package/lib/tui/dock-screen.ts +410 -0
- package/lib/tui/glyphs.ts +2 -1
- package/lib/tui/indent.ts +1 -1
- package/lib/tui/index.ts +2 -0
- package/lib/tui/rows.ts +21 -0
- package/lib/tui/status-controller.ts +35 -1
- package/lib/tui/tool.ts +59 -1
- package/lib/web/components.ts +20 -1
- package/lib/web/markup.ts +32 -0
- package/lib/web/runtime.ts +50 -0
- package/package.json +22 -5
- package/skills/init/SKILL.md +165 -0
- package/skills/init/references/agents-md-quality.md +103 -0
- package/skills/init/references/interview-questions.md +81 -0
- package/skills/refactor/SKILL.md +141 -0
- package/extensions/watch/process.ts +0 -63
- package/extensions/web-access/fetch/pdf.ts +0 -45
|
@@ -1,34 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The package-update snapshot, cached on disk with the time it was read.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* The 24-hour window is what makes a long-lived session different from a fresh
|
|
5
|
+
* one: a session that starts inside the window reuses the last read instead of
|
|
6
|
+
* repeating Pi's npm/git checks, and `/bytefly update` re-runs them on demand.
|
|
7
|
+
* The entry is keyed by the working directory and the effective package sources,
|
|
8
|
+
* so a project switch or an install/removal invalidates it instead of showing
|
|
9
|
+
* another project's list.
|
|
9
10
|
*
|
|
10
|
-
* A cache can only ever be a miss. An absent, unreadable,
|
|
11
|
-
* reads as `undefined` and the caller fetches; nothing here
|
|
12
|
-
* stale file must never become a session error.
|
|
11
|
+
* A cache can only ever be a miss. An absent, unreadable, old-schema, or
|
|
12
|
+
* mistyped file reads as `undefined` and the caller fetches; nothing here
|
|
13
|
+
* throws, because a stale file must never become a session error.
|
|
13
14
|
*/
|
|
14
15
|
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
15
17
|
import { readFileSync } from "node:fs";
|
|
16
18
|
import { join } from "node:path";
|
|
19
|
+
import type { PackageSource } from "@earendil-works/pi-coding-agent";
|
|
17
20
|
import { writeConfigFile } from "../../../lib/config.ts";
|
|
21
|
+
import type { PackageUpdate } from "./types.ts";
|
|
18
22
|
import { parseVersion } from "./version.ts";
|
|
19
23
|
|
|
20
24
|
/** The file name under `<agentDir>/bytefly/`. */
|
|
21
25
|
export const UPDATE_CACHE_FILE = "update.json";
|
|
22
26
|
|
|
23
|
-
/** How long a cached
|
|
27
|
+
/** How long a cached snapshot is reused before the next session re-reads. */
|
|
24
28
|
export const UPDATE_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
25
29
|
|
|
26
|
-
/** One cached
|
|
30
|
+
/** One cached snapshot: Pi's list, this package's version, and when it was read. */
|
|
27
31
|
export interface UpdateCacheEntry {
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
/** Epoch milliseconds of the read that produced
|
|
32
|
+
/** Identity of the cwd plus the effective package sources. */
|
|
33
|
+
key: string;
|
|
34
|
+
/** Epoch milliseconds of the read that produced this snapshot. */
|
|
31
35
|
checkedAt: number;
|
|
36
|
+
/** Every package Pi reported as updateable. */
|
|
37
|
+
updates: PackageUpdate[];
|
|
38
|
+
/** This package's new version, when it is one of the updates. */
|
|
39
|
+
latest?: string;
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
/** Where the cache lives for an agent directory. */
|
|
@@ -36,15 +44,41 @@ export function updateCachePath(agentDir: string): string {
|
|
|
36
44
|
return join(agentDir, "bytefly", UPDATE_CACHE_FILE);
|
|
37
45
|
}
|
|
38
46
|
|
|
39
|
-
/**
|
|
47
|
+
/** Stable identity for the settings a snapshot was read under. */
|
|
48
|
+
export function updateCacheKey(cwd: string, packages: readonly PackageSource[] | undefined): string {
|
|
49
|
+
return createHash("sha256")
|
|
50
|
+
.update(JSON.stringify({ cwd, packages: packages ?? [] }))
|
|
51
|
+
.digest("hex");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Whether a value has the fields Pi's package update carries. */
|
|
55
|
+
function isPackageUpdate(value: unknown): value is PackageUpdate {
|
|
56
|
+
if (!value || typeof value !== "object") return false;
|
|
57
|
+
const update = value as Partial<PackageUpdate>;
|
|
58
|
+
return (
|
|
59
|
+
typeof update.source === "string" &&
|
|
60
|
+
typeof update.displayName === "string" &&
|
|
61
|
+
(update.type === "npm" || update.type === "git") &&
|
|
62
|
+
(update.scope === "user" || update.scope === "project")
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Read the cache; a missing, corrupt, old-schema, or mistyped entry is `undefined`. */
|
|
40
67
|
export function readUpdateCache(path: string): UpdateCacheEntry | undefined {
|
|
41
68
|
try {
|
|
42
69
|
const raw = JSON.parse(readFileSync(path, "utf-8")) as Record<string, unknown> | undefined;
|
|
43
70
|
if (!raw || typeof raw !== "object") return undefined;
|
|
44
|
-
const
|
|
71
|
+
const key = typeof raw.key === "string" ? raw.key.trim() : "";
|
|
45
72
|
const checkedAt = typeof raw.checkedAt === "number" && Number.isFinite(raw.checkedAt) ? raw.checkedAt : 0;
|
|
46
|
-
|
|
47
|
-
|
|
73
|
+
const updates = Array.isArray(raw.updates) ? raw.updates : undefined;
|
|
74
|
+
if (!key || checkedAt <= 0 || !updates || !updates.every(isPackageUpdate)) return undefined;
|
|
75
|
+
const latest = typeof raw.latest === "string" ? raw.latest.trim() : "";
|
|
76
|
+
return {
|
|
77
|
+
key,
|
|
78
|
+
checkedAt,
|
|
79
|
+
updates,
|
|
80
|
+
...(parseVersion(latest) ? { latest } : {}),
|
|
81
|
+
};
|
|
48
82
|
} catch {
|
|
49
83
|
return undefined;
|
|
50
84
|
}
|
|
@@ -55,7 +89,12 @@ export function readUpdateCache(path: string): UpdateCacheEntry | undefined {
|
|
|
55
89
|
* treats a throw as a miss: a read-only agent directory must not break a session.
|
|
56
90
|
*/
|
|
57
91
|
export function writeUpdateCache(path: string, entry: UpdateCacheEntry): void {
|
|
58
|
-
writeConfigFile(path, {
|
|
92
|
+
writeConfigFile(path, {
|
|
93
|
+
key: entry.key,
|
|
94
|
+
checkedAt: entry.checkedAt,
|
|
95
|
+
updates: entry.updates,
|
|
96
|
+
...(entry.latest ? { latest: entry.latest } : {}),
|
|
97
|
+
});
|
|
59
98
|
}
|
|
60
99
|
|
|
61
100
|
/**
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* never sees an error from a check nobody asked for.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { DefaultPackageManager, getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import type { PackageUpdate, PackageUpdateCheck } from "./types.ts";
|
|
13
15
|
import { parseVersion } from "./version.ts";
|
|
14
16
|
|
|
15
17
|
/** The published document holding the version npm's `latest` tag points at. */
|
|
@@ -37,6 +39,39 @@ export interface UpdateCheckDeps {
|
|
|
37
39
|
timeoutMs?: number;
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
/** What the package-manager read needs. */
|
|
43
|
+
export interface AvailableUpdatesDeps {
|
|
44
|
+
/** Session working directory. */
|
|
45
|
+
cwd: string;
|
|
46
|
+
/** Pi agent directory; defaults to `getAgentDir()`. */
|
|
47
|
+
agentDir?: string;
|
|
48
|
+
/** Whether project-local packages may be read. */
|
|
49
|
+
projectTrusted: boolean;
|
|
50
|
+
/** The real check captured before suppression; defaults to the manager method. */
|
|
51
|
+
checkForAvailableUpdates?: PackageUpdateCheck;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Every package Pi would update, or `undefined` when the check cannot run.
|
|
56
|
+
*
|
|
57
|
+
* The distinction is load-bearing: an empty list is a real answer ("everything is
|
|
58
|
+
* current") that the caller caches for a day, while a failed read must leave the
|
|
59
|
+
* previous snapshot and the header marker alone.
|
|
60
|
+
*/
|
|
61
|
+
export async function fetchAvailableUpdates(deps: AvailableUpdatesDeps): Promise<PackageUpdate[] | undefined> {
|
|
62
|
+
try {
|
|
63
|
+
const agentDir = deps.agentDir ?? getAgentDir();
|
|
64
|
+
const settingsManager = SettingsManager.create(deps.cwd, agentDir, {
|
|
65
|
+
projectTrusted: deps.projectTrusted,
|
|
66
|
+
});
|
|
67
|
+
const manager = new DefaultPackageManager({ cwd: deps.cwd, agentDir, settingsManager });
|
|
68
|
+
const check = deps.checkForAvailableUpdates ?? ((current) => current.checkForAvailableUpdates());
|
|
69
|
+
return await check(manager);
|
|
70
|
+
} catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
40
75
|
/** The latest published version, or `undefined` when it cannot be read. */
|
|
41
76
|
export async function fetchLatestVersion(deps: UpdateCheckDeps = {}): Promise<string | undefined> {
|
|
42
77
|
const request: FetchLike = deps.fetch ?? ((url, init) => globalThis.fetch(url, init));
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Wording for the update
|
|
3
|
-
* the answer an explicit re-check prints.
|
|
2
|
+
* Wording for the update surface: the header marker, the one transcript notice,
|
|
3
|
+
* and the answer an explicit re-check prints.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* says what it just found, including the two cases it cannot answer (offline, and
|
|
10
|
-
* a registry it could not read).
|
|
5
|
+
* Pi's package manager decides which configured packages are behind. The marker
|
|
6
|
+
* is still only this package's newest version (`↑0.12.0`); the notice names every
|
|
7
|
+
* package Pi reported and, for this package, the version arrow and release URL
|
|
8
|
+
* the notice renderer can print as text.
|
|
11
9
|
*/
|
|
12
10
|
|
|
13
11
|
import pkg from "../../../package.json";
|
|
14
12
|
import { noticeMessage, type NoticeMessage } from "../../../lib/notice.ts";
|
|
15
13
|
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
16
14
|
import type { UpdateOutcome } from "./state.ts";
|
|
15
|
+
import type { PackageUpdate } from "./types.ts";
|
|
17
16
|
|
|
18
|
-
/** The header marker: an up arrow and
|
|
17
|
+
/** The header marker: an up arrow and this package's newest version. */
|
|
19
18
|
export function updateMarker(latest: string): string {
|
|
20
19
|
return `${GLYPHS.update}${latest}`;
|
|
21
20
|
}
|
|
22
21
|
|
|
22
|
+
/** The command that resolves every row in the notice. */
|
|
23
|
+
export const UPDATE_COMMAND = "pi update --extensions";
|
|
24
|
+
|
|
23
25
|
/**
|
|
24
26
|
* The release page for a version, or `undefined` when this checkout does not name
|
|
25
27
|
* a GitHub repository. Derived from `package.json` so a rename cannot leave a dead
|
|
@@ -32,35 +34,72 @@ export function releaseUrl(version: string): string | undefined {
|
|
|
32
34
|
return `${base}/releases/tag/v${version}`;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
/**
|
|
36
|
-
export function
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
/** Whether a row is this package, whose version the header and notice show. */
|
|
38
|
+
export function isOwnPackage(update: PackageUpdate): boolean {
|
|
39
|
+
return update.type === "npm" && update.displayName === pkg.name;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The scope word Pi's package update carries. */
|
|
43
|
+
function scopeLabel(update: PackageUpdate): string {
|
|
44
|
+
return update.scope === "project" ? "project" : "global";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** One package row: the version arrow for this package, the scope for the rest. */
|
|
48
|
+
function packageRow(
|
|
49
|
+
update: PackageUpdate,
|
|
50
|
+
current: string,
|
|
51
|
+
ownLatest?: string,
|
|
52
|
+
): {
|
|
53
|
+
label: string;
|
|
54
|
+
detail: string;
|
|
55
|
+
severity: "info";
|
|
56
|
+
} {
|
|
57
|
+
const detail = isOwnPackage(update) && ownLatest ? `v${current} → v${ownLatest}` : scopeLabel(update);
|
|
58
|
+
return { label: update.displayName, detail, severity: "info" };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The headline shared by the notice and the re-check answer. */
|
|
62
|
+
function headline(count: number): string {
|
|
63
|
+
return count === 1 ? "1 package update available" : `${count} package updates available`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The headline and the package names, the sentence both answers open with. */
|
|
67
|
+
function namesLine(updates: readonly PackageUpdate[]): string {
|
|
68
|
+
return `${headline(updates.length)}: ${updates.map((update) => update.displayName).join(", ")}.`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The release URL for this package's new version, when it is known. */
|
|
72
|
+
function ownRelease(ownLatest?: string): string | undefined {
|
|
73
|
+
return ownLatest ? releaseUrl(ownLatest) : undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The single notice a session posts when Pi reports package updates. */
|
|
77
|
+
export function updateNotice(updates: readonly PackageUpdate[], current: string, ownLatest?: string): NoticeMessage {
|
|
78
|
+
const own = updates.find(isOwnPackage);
|
|
79
|
+
const release = own ? ownRelease(ownLatest) : undefined;
|
|
80
|
+
const parts = [namesLine(updates)];
|
|
81
|
+
if (own && ownLatest) parts.push(`${pkg.name} v${current} → v${ownLatest}.`);
|
|
82
|
+
parts.push(`Run \`${UPDATE_COMMAND}\` to update.`);
|
|
83
|
+
if (release) parts.push(`Release notes: ${release}`);
|
|
39
84
|
return noticeMessage({
|
|
40
|
-
content:
|
|
41
|
-
`bytefly ${latest} is available (current ${current}). Run \`${command}\` to update.` +
|
|
42
|
-
(release ? ` Release notes: ${release}` : ""),
|
|
85
|
+
content: parts.join(" "),
|
|
43
86
|
details: {
|
|
44
87
|
source: "bytefly",
|
|
45
|
-
title:
|
|
88
|
+
title: headline(updates.length),
|
|
46
89
|
severity: "info",
|
|
47
|
-
items:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
]
|
|
53
|
-
: [
|
|
54
|
-
{ label: "current", detail: `v${current}` },
|
|
55
|
-
{ label: "update", detail: command },
|
|
56
|
-
],
|
|
90
|
+
items: [
|
|
91
|
+
...updates.map((update) => packageRow(update, current, ownLatest)),
|
|
92
|
+
{ label: "update", detail: UPDATE_COMMAND },
|
|
93
|
+
...(release ? [{ label: "release", detail: release }] : []),
|
|
94
|
+
],
|
|
57
95
|
},
|
|
58
96
|
});
|
|
59
97
|
}
|
|
60
98
|
|
|
61
99
|
/**
|
|
62
100
|
* What `/bytefly update` prints. The background check is silent because nobody
|
|
63
|
-
* asked; here someone did, so a skip and
|
|
101
|
+
* asked; here someone did, so a skip, a failure, and an up-to-date answer are
|
|
102
|
+
* answers too.
|
|
64
103
|
*/
|
|
65
104
|
export function updateOutcomeText(
|
|
66
105
|
outcome: UpdateOutcome,
|
|
@@ -71,17 +110,19 @@ export function updateOutcomeText(
|
|
|
71
110
|
return { text: "bytefly: the update check was skipped (PI_OFFLINE is set).", severity: "info" };
|
|
72
111
|
case "failed":
|
|
73
112
|
return {
|
|
74
|
-
text: "bytefly: could not
|
|
113
|
+
text: "bytefly: could not check package updates — check the network and try again.",
|
|
75
114
|
severity: "warning",
|
|
76
115
|
};
|
|
77
116
|
case "current":
|
|
78
|
-
return { text:
|
|
117
|
+
return { text: "bytefly: up to date — no configured package has an update.", severity: "info" };
|
|
79
118
|
case "available": {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
};
|
|
119
|
+
const own = outcome.updates.some(isOwnPackage);
|
|
120
|
+
const release = own ? ownRelease(outcome.ownLatest) : undefined;
|
|
121
|
+
const parts = [`bytefly: ${namesLine(outcome.updates)}`];
|
|
122
|
+
if (own && outcome.ownLatest) parts.push(`This package is v${current} → v${outcome.ownLatest}.`);
|
|
123
|
+
parts.push(`Run ${UPDATE_COMMAND}.`);
|
|
124
|
+
if (release) parts.push(`Release notes: ${release}`);
|
|
125
|
+
return { text: parts.join(" "), severity: "info" };
|
|
85
126
|
}
|
|
86
127
|
}
|
|
87
128
|
}
|
|
@@ -1,47 +1,57 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The update
|
|
3
|
-
*
|
|
2
|
+
* The update surface's lifecycle: ask Pi's package manager which configured
|
|
3
|
+
* packages are behind, cache the snapshot, and announce it once per load.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Pi would draw its own `Package Updates Available` block from the same method
|
|
6
|
+
* after `session_start`. bytefly suppresses that block, asks the captured
|
|
7
|
+
* original check itself, and draws the single notice. The request is fired
|
|
8
|
+
* without awaiting: Pi awaits `session_start` handlers, and a slow npm/git check
|
|
9
|
+
* must not hold the first prompt.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* version: an available update is not a prerequisite problem.
|
|
11
|
+
* The 24-hour cache is keyed by the working directory and the effective package
|
|
12
|
+
* sources, so a project switch or an install/removal invalidates it. A cache hit
|
|
13
|
+
* costs no package-manager call; `/bytefly update` ignores the window and re-runs
|
|
14
|
+
* the check on demand.
|
|
16
15
|
*/
|
|
17
16
|
|
|
18
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
19
18
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
20
19
|
import pkg from "../../../package.json";
|
|
21
20
|
import { isOffline } from "../../../lib/env.ts";
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
import {
|
|
22
|
+
isFresh,
|
|
23
|
+
readUpdateCache,
|
|
24
|
+
updateCacheKey,
|
|
25
|
+
updateCachePath,
|
|
26
|
+
writeUpdateCache,
|
|
27
|
+
type UpdateCacheEntry,
|
|
28
|
+
} from "./cache.ts";
|
|
29
|
+
import { fetchAvailableUpdates, fetchLatestVersion } from "./check.ts";
|
|
30
|
+
import { isOwnPackage, updateNotice } from "./format.ts";
|
|
25
31
|
import type { UpdateOutcome, UpdateSource } from "./state.ts";
|
|
32
|
+
import { suppressPiPackageUpdates, type UpdateSuppression } from "./suppress.ts";
|
|
33
|
+
import type { PackageUpdate, PackageUpdateCheck } from "./types.ts";
|
|
26
34
|
import { isNewerVersion } from "./version.ts";
|
|
27
35
|
|
|
28
36
|
/** The cache, as the lifecycle sees it: read on start, written after a read. */
|
|
29
37
|
export interface UpdateCache {
|
|
30
|
-
/** The last recorded
|
|
38
|
+
/** The last recorded snapshot, or `undefined` for a miss. */
|
|
31
39
|
read(): UpdateCacheEntry | undefined;
|
|
32
|
-
/** Record
|
|
40
|
+
/** Record a snapshot; a throw is treated as a miss. */
|
|
33
41
|
write(entry: UpdateCacheEntry): void;
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
/** What `registerUpdate` returns, so the panel can re-run the check on demand. */
|
|
37
45
|
export interface UpdateHandle {
|
|
38
|
-
/** Read the
|
|
46
|
+
/** Read the package list now, ignoring the cache window. Never throws. */
|
|
39
47
|
recheck(): Promise<UpdateOutcome>;
|
|
40
48
|
}
|
|
41
49
|
|
|
42
|
-
/** Test seams; production uses
|
|
50
|
+
/** Test seams; production uses Pi's package manager, its cache, and the agent dir. */
|
|
43
51
|
export interface UpdateDeps {
|
|
44
|
-
/** Replace the
|
|
52
|
+
/** Replace the package-manager read. */
|
|
53
|
+
readUpdates?: (ctx: ExtensionContext, check: PackageUpdateCheck) => Promise<PackageUpdate[] | undefined>;
|
|
54
|
+
/** Replace the registry read that names this package's new version. */
|
|
45
55
|
fetchLatest?: () => Promise<string | undefined>;
|
|
46
56
|
/** Replace the `PI_OFFLINE` check. */
|
|
47
57
|
isOffline?: () => boolean;
|
|
@@ -49,7 +59,9 @@ export interface UpdateDeps {
|
|
|
49
59
|
cache?: UpdateCache;
|
|
50
60
|
/** Replace the clock the cache timestamp is read from. */
|
|
51
61
|
now?: () => number;
|
|
52
|
-
/**
|
|
62
|
+
/** Replace the host suppression; defaults to patching Pi's package manager. */
|
|
63
|
+
suppress?: () => UpdateSuppression;
|
|
64
|
+
/** Called once a snapshot is recorded, so a mounted surface repaints. */
|
|
53
65
|
onUpdate?: () => void;
|
|
54
66
|
}
|
|
55
67
|
|
|
@@ -61,52 +73,112 @@ function diskCache(): UpdateCache {
|
|
|
61
73
|
try {
|
|
62
74
|
writeUpdateCache(updateCachePath(getAgentDir()), entry);
|
|
63
75
|
} catch {
|
|
64
|
-
// A read-only agent directory costs the next session one
|
|
76
|
+
// A read-only agent directory costs the next session one check, nothing more.
|
|
65
77
|
}
|
|
66
78
|
},
|
|
67
79
|
};
|
|
68
80
|
}
|
|
69
81
|
|
|
70
82
|
/**
|
|
71
|
-
* Register the
|
|
72
|
-
* a
|
|
83
|
+
* Register the update lifecycle. `updates` is the store the header reads, so
|
|
84
|
+
* recording a snapshot is what puts this package's marker on screen.
|
|
73
85
|
*/
|
|
74
86
|
export function registerUpdate(pi: ExtensionAPI, updates: UpdateSource, deps: UpdateDeps = {}): UpdateHandle {
|
|
87
|
+
const readUpdates =
|
|
88
|
+
deps.readUpdates ??
|
|
89
|
+
((ctx, check) =>
|
|
90
|
+
fetchAvailableUpdates({
|
|
91
|
+
cwd: ctx.cwd,
|
|
92
|
+
agentDir: getAgentDir(),
|
|
93
|
+
projectTrusted: ctx.isProjectTrusted(),
|
|
94
|
+
checkForAvailableUpdates: check,
|
|
95
|
+
}));
|
|
75
96
|
const readLatest = deps.fetchLatest ?? (() => fetchLatestVersion());
|
|
76
97
|
const offline = deps.isOffline ?? isOffline;
|
|
77
98
|
const cache = deps.cache ?? diskCache();
|
|
78
99
|
const now = deps.now ?? Date.now;
|
|
100
|
+
const suppress = deps.suppress ?? suppressPiPackageUpdates;
|
|
79
101
|
// A session is announced at most once per load, even if `session_start` fires
|
|
80
102
|
// again after a resume or a session replacement.
|
|
81
103
|
let announced = false;
|
|
82
104
|
let generation = 0;
|
|
105
|
+
let suppression: UpdateSuppression | undefined;
|
|
106
|
+
let context: ExtensionContext | undefined;
|
|
107
|
+
const fallbackCheck: PackageUpdateCheck = (manager) => manager.checkForAvailableUpdates();
|
|
108
|
+
const activeCheck = (): PackageUpdateCheck => suppression?.check ?? fallbackCheck;
|
|
83
109
|
|
|
84
|
-
/**
|
|
85
|
-
const
|
|
86
|
-
|
|
110
|
+
/** Identity of the settings a snapshot is read under. */
|
|
111
|
+
const keyFor = (ctx: ExtensionContext): string => updateCacheKey(ctx.cwd, pi.getSettings?.()?.packages);
|
|
112
|
+
|
|
113
|
+
/** A cache write never turns a read into a failed check. */
|
|
114
|
+
const writeCache = (entry: UpdateCacheEntry): void => {
|
|
115
|
+
try {
|
|
116
|
+
cache.write(entry);
|
|
117
|
+
} catch {
|
|
118
|
+
// The next session performs one more check; this one already has its answer.
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/** This package's new version in a snapshot, when it is actually newer. */
|
|
123
|
+
const ownLatestOf = (entry: UpdateCacheEntry): string | undefined => {
|
|
124
|
+
if (!entry.latest || !isNewerVersion(entry.latest, pkg.version)) return undefined;
|
|
125
|
+
return entry.updates.some(isOwnPackage) ? entry.latest : undefined;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/** Paint the header marker, or clear it when this package is current. */
|
|
129
|
+
const paint = (latest?: string): void => {
|
|
130
|
+
if (updates.latest() === latest) return;
|
|
87
131
|
updates.set(latest);
|
|
88
132
|
deps.onUpdate?.();
|
|
89
133
|
};
|
|
90
134
|
|
|
91
|
-
/** The once-per-load transcript notice, posted only for a
|
|
92
|
-
const announce = (
|
|
93
|
-
if (announced ||
|
|
135
|
+
/** The once-per-load transcript notice, posted only for a non-empty list. */
|
|
136
|
+
const announce = (entry: UpdateCacheEntry): void => {
|
|
137
|
+
if (announced || entry.updates.length === 0) return;
|
|
94
138
|
announced = true;
|
|
95
|
-
pi.sendMessage(updateNotice(pkg.version,
|
|
139
|
+
pi.sendMessage(updateNotice(entry.updates, pkg.version, ownLatestOf(entry)));
|
|
96
140
|
};
|
|
97
141
|
|
|
98
|
-
/** One
|
|
142
|
+
/** One snapshot: paint the marker and, when it has rows, announce it once. */
|
|
99
143
|
const record = (entry: UpdateCacheEntry): void => {
|
|
100
|
-
paint(entry
|
|
101
|
-
announce(entry
|
|
144
|
+
paint(ownLatestOf(entry));
|
|
145
|
+
announce(entry);
|
|
102
146
|
};
|
|
103
147
|
|
|
104
|
-
|
|
148
|
+
/** Offline: the cached marker is advisory, but no notice is posted. */
|
|
149
|
+
const recordMarkerOnly = (entry: UpdateCacheEntry): void => {
|
|
150
|
+
paint(ownLatestOf(entry));
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** Resolve this package's version label, dropping an equal or older answer. */
|
|
154
|
+
const resolveOwnLatest = async (own: PackageUpdate | undefined): Promise<string | undefined> => {
|
|
155
|
+
if (!own) return undefined;
|
|
156
|
+
let latest: string | undefined;
|
|
105
157
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
158
|
+
latest = await readLatest();
|
|
159
|
+
} catch {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
return latest && isNewerVersion(latest, pkg.version) ? latest : undefined;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const check = async (ctx: ExtensionContext, token: number): Promise<void> => {
|
|
166
|
+
try {
|
|
167
|
+
const list = await readUpdates(ctx, activeCheck());
|
|
168
|
+
if (token !== generation) return;
|
|
169
|
+
// A check that could not run is not an empty answer: caching it would tell
|
|
170
|
+
// the next session "up to date" for a day and clear a marker that may still
|
|
171
|
+
// be right, so leave both alone and try again next time.
|
|
172
|
+
if (list === undefined) return;
|
|
173
|
+
const latest = await resolveOwnLatest(list.find(isOwnPackage));
|
|
174
|
+
if (token !== generation) return;
|
|
175
|
+
const entry: UpdateCacheEntry = {
|
|
176
|
+
key: keyFor(ctx),
|
|
177
|
+
checkedAt: now(),
|
|
178
|
+
updates: list,
|
|
179
|
+
...(latest ? { latest } : {}),
|
|
180
|
+
};
|
|
181
|
+
writeCache(entry);
|
|
110
182
|
record(entry);
|
|
111
183
|
} catch {
|
|
112
184
|
// A background check never surfaces an error: the session does not know it ran.
|
|
@@ -114,45 +186,62 @@ export function registerUpdate(pi: ExtensionAPI, updates: UpdateSource, deps: Up
|
|
|
114
186
|
};
|
|
115
187
|
|
|
116
188
|
pi.on("session_start", (_event, ctx) => {
|
|
117
|
-
//
|
|
118
|
-
//
|
|
189
|
+
// `/bytefly update` can answer in any mode; only the TUI installs the host
|
|
190
|
+
// shim and posts a notice.
|
|
191
|
+
context = ctx;
|
|
119
192
|
if (ctx.mode !== "tui") return;
|
|
193
|
+
suppression ??= suppress();
|
|
194
|
+
const cached = cache.read();
|
|
195
|
+
const key = keyFor(ctx);
|
|
120
196
|
if (offline()) {
|
|
121
|
-
// No request, so the window does not matter: the cached
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
if (cached) record(cached);
|
|
197
|
+
// No request, so the window does not matter: the cached marker is the best
|
|
198
|
+
// answer available, and no notice is posted from a stale list.
|
|
199
|
+
if (cached && cached.key === key) recordMarkerOnly(cached);
|
|
125
200
|
return;
|
|
126
201
|
}
|
|
127
|
-
|
|
128
|
-
if (cached && isFresh(cached, now())) {
|
|
202
|
+
if (cached && cached.key === key && isFresh(cached, now())) {
|
|
129
203
|
record(cached);
|
|
130
204
|
return;
|
|
131
205
|
}
|
|
132
206
|
if (announced) return;
|
|
133
|
-
void check(generation);
|
|
207
|
+
void check(ctx, generation);
|
|
134
208
|
});
|
|
135
209
|
|
|
136
210
|
pi.on("session_shutdown", () => {
|
|
137
|
-
// Invalidate any request still in flight
|
|
211
|
+
// Invalidate any request still in flight, drop what this session found, and
|
|
212
|
+
// give Pi's own check back to the host.
|
|
138
213
|
generation++;
|
|
139
214
|
updates.set();
|
|
215
|
+
suppression?.restore();
|
|
216
|
+
suppression = undefined;
|
|
217
|
+
context = undefined;
|
|
140
218
|
});
|
|
141
219
|
|
|
142
220
|
return {
|
|
143
221
|
async recheck() {
|
|
144
222
|
if (offline()) return { kind: "offline" };
|
|
145
|
-
|
|
223
|
+
if (!context) return { kind: "failed" };
|
|
224
|
+
let list: PackageUpdate[];
|
|
146
225
|
try {
|
|
147
|
-
|
|
226
|
+
const found = await readUpdates(context, activeCheck());
|
|
227
|
+
if (found === undefined) return { kind: "failed" };
|
|
228
|
+
list = found;
|
|
148
229
|
} catch {
|
|
149
230
|
return { kind: "failed" };
|
|
150
231
|
}
|
|
151
|
-
|
|
152
|
-
|
|
232
|
+
const latest = await resolveOwnLatest(list.find(isOwnPackage));
|
|
233
|
+
const entry: UpdateCacheEntry = {
|
|
234
|
+
key: keyFor(context),
|
|
235
|
+
checkedAt: now(),
|
|
236
|
+
updates: list,
|
|
237
|
+
...(latest ? { latest } : {}),
|
|
238
|
+
};
|
|
239
|
+
writeCache(entry);
|
|
153
240
|
// The caller prints the answer, so this path never posts a notice.
|
|
154
241
|
paint(latest);
|
|
155
|
-
return
|
|
242
|
+
return list.length > 0
|
|
243
|
+
? { kind: "available", updates: list, ...(latest ? { ownLatest: latest } : {}) }
|
|
244
|
+
: { kind: "current" };
|
|
156
245
|
},
|
|
157
246
|
};
|
|
158
247
|
}
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* cleared on shutdown, so a reload starts from nothing.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import type { PackageUpdate } from "./types.ts";
|
|
13
|
+
|
|
12
14
|
/** The store the header reads and the update lifecycle writes. */
|
|
13
15
|
export interface UpdateSource {
|
|
14
16
|
/** The newer published version, or `undefined` when none is known. */
|
|
@@ -25,8 +27,8 @@ export interface UpdateSource {
|
|
|
25
27
|
export type UpdateOutcome =
|
|
26
28
|
| { kind: "offline" }
|
|
27
29
|
| { kind: "failed" }
|
|
28
|
-
| { kind: "current"
|
|
29
|
-
| { kind: "available";
|
|
30
|
+
| { kind: "current" }
|
|
31
|
+
| { kind: "available"; updates: readonly PackageUpdate[]; ownLatest?: string };
|
|
30
32
|
|
|
31
33
|
/** A store holding at most one version. */
|
|
32
34
|
export function createUpdateState(): UpdateSource {
|