pi-updater 0.2.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 +38 -0
- package/index.ts +210 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# pi-updater
|
|
2
|
+
|
|
3
|
+
A Codex-style auto-updater for Pi.
|
|
4
|
+
|
|
5
|
+
> **Note:** Currently supports npm installations only.
|
|
6
|
+
|
|
7
|
+
<img width="800" height="482" alt="Screenshot 2026-02-28 at 09 01 37" src="https://github.com/user-attachments/assets/89df2dad-8d91-464b-b3cb-dfd15bce1c06" />
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
**On startup:** if a newer version is available, shows a prompt:
|
|
12
|
+
- **Update now** — install with npm, then restart pi
|
|
13
|
+
- **Skip** — dismiss until next session
|
|
14
|
+
- **Skip this version** — don't ask again until a newer version appears
|
|
15
|
+
|
|
16
|
+
**`/update`:** manually check for updates (always fetches fresh from npm)
|
|
17
|
+
|
|
18
|
+
Version checks are cached. Latest version is fetched in the background on startup.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pi install git:github.com/tonze/pi-updater
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
Use `/update` inside pi to manually check for updates and install them.
|
|
29
|
+
|
|
30
|
+
## Updating this package
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pi update
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@mariozechner/pi-coding-agent";
|
|
5
|
+
import { VERSION, BorderedLoader } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
7
|
+
import { join, dirname } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
|
|
10
|
+
const PACKAGE_NAME = "@mariozechner/pi-coding-agent";
|
|
11
|
+
const REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
12
|
+
const CACHE_FILE = join(homedir(), ".pi", "agent", "update-cache.json");
|
|
13
|
+
|
|
14
|
+
interface VersionCache {
|
|
15
|
+
latestVersion: string;
|
|
16
|
+
dismissedVersion?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readCache(): VersionCache | undefined {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(readFileSync(CACHE_FILE, "utf-8"));
|
|
22
|
+
} catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function writeCache(cache: VersionCache) {
|
|
28
|
+
try {
|
|
29
|
+
mkdirSync(dirname(CACHE_FILE), { recursive: true });
|
|
30
|
+
writeFileSync(CACHE_FILE, JSON.stringify(cache) + "\n");
|
|
31
|
+
} catch {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseVersion(v: string): [number, number, number] | undefined {
|
|
35
|
+
const parts = v.trim().split(".");
|
|
36
|
+
if (parts.length !== 3) return undefined;
|
|
37
|
+
const nums = parts.map(Number);
|
|
38
|
+
if (nums.some(isNaN)) return undefined;
|
|
39
|
+
return nums as [number, number, number];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isNewer(latest: string, current: string): boolean {
|
|
43
|
+
const l = parseVersion(latest);
|
|
44
|
+
const c = parseVersion(current);
|
|
45
|
+
if (!l || !c) return false;
|
|
46
|
+
if (l[0] !== c[0]) return l[0] > c[0];
|
|
47
|
+
if (l[1] !== c[1]) return l[1] > c[1];
|
|
48
|
+
return l[2] > c[2];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function fetchLatestVersion(): Promise<string | undefined> {
|
|
52
|
+
try {
|
|
53
|
+
const res = await fetch(REGISTRY_URL, {
|
|
54
|
+
signal: AbortSignal.timeout(10_000),
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) return undefined;
|
|
57
|
+
return ((await res.json()) as { version?: string }).version;
|
|
58
|
+
} catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Returns the cached latest version if an upgrade is available and not dismissed.
|
|
65
|
+
* Always kicks off a background fetch to refresh the cache for the next run.
|
|
66
|
+
*/
|
|
67
|
+
function getUpgradeVersion(): string | undefined {
|
|
68
|
+
const cache = readCache();
|
|
69
|
+
|
|
70
|
+
void fetchLatestVersion().then((latest) => {
|
|
71
|
+
if (!latest) return;
|
|
72
|
+
// Re-read cache to avoid overwriting a dismissal that happened during the fetch
|
|
73
|
+
writeCache({
|
|
74
|
+
latestVersion: latest,
|
|
75
|
+
dismissedVersion: readCache()?.dismissedVersion,
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!cache) return undefined;
|
|
80
|
+
if (!isNewer(cache.latestVersion, VERSION)) return undefined;
|
|
81
|
+
if (cache.dismissedVersion === cache.latestVersion) return undefined;
|
|
82
|
+
return cache.latestVersion;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function dismissVersion(version: string) {
|
|
86
|
+
const cache = readCache();
|
|
87
|
+
if (!cache) return;
|
|
88
|
+
cache.dismissedVersion = version;
|
|
89
|
+
writeCache(cache);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getInstallCommand(version: string): { program: string; args: string[] } {
|
|
93
|
+
return {
|
|
94
|
+
program: "npm",
|
|
95
|
+
args: ["install", "-g", `${PACKAGE_NAME}@${version}`],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function fmtCmd(cmd: { program: string; args: string[] }): string {
|
|
100
|
+
return `${cmd.program} ${cmd.args.join(" ")}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default function (pi: ExtensionAPI) {
|
|
104
|
+
async function doInstall(
|
|
105
|
+
ctx: ExtensionContext,
|
|
106
|
+
latest: string,
|
|
107
|
+
cmd: { program: string; args: string[] },
|
|
108
|
+
) {
|
|
109
|
+
const success = await ctx.ui.custom<boolean>((tui, theme, _kb, done) => {
|
|
110
|
+
const loader = new BorderedLoader(tui, theme, `Installing ${latest}...`);
|
|
111
|
+
loader.onAbort = () => done(false);
|
|
112
|
+
|
|
113
|
+
const run = async () => {
|
|
114
|
+
if (cmd.program === "echo") {
|
|
115
|
+
ctx.ui.notify(cmd.args[0], "info");
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
const result = await pi.exec(cmd.program, cmd.args, {
|
|
119
|
+
timeout: 120_000,
|
|
120
|
+
});
|
|
121
|
+
if (result.code !== 0) {
|
|
122
|
+
ctx.ui.notify(
|
|
123
|
+
`Update failed (exit ${result.code}): ${result.stderr || result.stdout}`,
|
|
124
|
+
"error",
|
|
125
|
+
);
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
run()
|
|
132
|
+
.then(done)
|
|
133
|
+
.catch(() => done(false));
|
|
134
|
+
return loader;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (!success) return;
|
|
138
|
+
|
|
139
|
+
const ok = await ctx.ui.confirm(
|
|
140
|
+
`Updated to ${latest}!`,
|
|
141
|
+
"Shut down pi? (Use pi -c to continue this session)",
|
|
142
|
+
);
|
|
143
|
+
if (ok) ctx.shutdown();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function showUpdatePrompt(ctx: ExtensionContext, latest: string) {
|
|
147
|
+
const cmd = getInstallCommand(latest);
|
|
148
|
+
const choice = await ctx.ui.select(`Update ${VERSION} → ${latest}`, [
|
|
149
|
+
`Update now (${fmtCmd(cmd)})`,
|
|
150
|
+
"Skip",
|
|
151
|
+
"Skip this version",
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
if (!choice || choice === "Skip") return;
|
|
155
|
+
if (choice === "Skip this version") {
|
|
156
|
+
dismissVersion(latest);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
await doInstall(ctx, latest, cmd);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
163
|
+
if (!ctx.hasUI) return;
|
|
164
|
+
const latest = getUpgradeVersion();
|
|
165
|
+
if (latest) void showUpdatePrompt(ctx, latest);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
pi.on("session_switch", async (_event, ctx) => {
|
|
169
|
+
if (!ctx.hasUI) return;
|
|
170
|
+
const latest = getUpgradeVersion();
|
|
171
|
+
if (latest) void showUpdatePrompt(ctx, latest);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
pi.registerCommand("update", {
|
|
175
|
+
description: "Check for pi updates and install",
|
|
176
|
+
handler: async (_args, ctx) => {
|
|
177
|
+
const latest = await ctx.ui.custom<string | null>(
|
|
178
|
+
(tui, theme, _kb, done) => {
|
|
179
|
+
const loader = new BorderedLoader(
|
|
180
|
+
tui,
|
|
181
|
+
theme,
|
|
182
|
+
"Checking for updates...",
|
|
183
|
+
);
|
|
184
|
+
loader.onAbort = () => done(null);
|
|
185
|
+
fetchLatestVersion()
|
|
186
|
+
.then((v) => done(v ?? null))
|
|
187
|
+
.catch(() => done(null));
|
|
188
|
+
return loader;
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
if (!latest) {
|
|
193
|
+
ctx.ui.notify("Could not reach npm registry.", "error");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
writeCache({
|
|
198
|
+
latestVersion: latest,
|
|
199
|
+
dismissedVersion: readCache()?.dismissedVersion,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (!isNewer(latest, VERSION)) {
|
|
203
|
+
ctx.ui.notify(`Already on latest version (${VERSION}).`, "info");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
await showUpdatePrompt(ctx, latest);
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-updater",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A /update command for pi — check for new versions, install updates with npm, and resume with pi -c after restart",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Toms clanker",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/tonze/pi-updater.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"pi-package",
|
|
14
|
+
"update"
|
|
15
|
+
],
|
|
16
|
+
"pi": {
|
|
17
|
+
"extensions": [
|
|
18
|
+
"./index.ts"
|
|
19
|
+
],
|
|
20
|
+
"image": "https://github.com/user-attachments/assets/89df2dad-8d91-464b-b3cb-dfd15bce1c06"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"index.ts",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@mariozechner/pi-coding-agent": "*"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@mariozechner/pi-coding-agent": "^0.55.1",
|
|
31
|
+
"@types/node": "^25.3.2",
|
|
32
|
+
"typescript": "^5.9.3"
|
|
33
|
+
}
|
|
34
|
+
}
|