pi-updater 0.2.8 → 0.3.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/CHANGELOG.md +17 -0
- package/README.md +33 -5
- package/index.ts +199 -52
- package/package.json +3 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0 - 2026-03-23
|
|
4
|
+
|
|
5
|
+
- Auto-restart pi after a successful update. Asks to restart, then seamlessly relaunches on the current session.
|
|
6
|
+
- Falls back to manual restart message in non-interactive modes or if restart fails.
|
|
7
|
+
- Cross-platform: uses `shell: true` on Windows to handle `.cmd` shims.
|
|
8
|
+
- `/update --test` to simulate the full update flow without a real install.
|
|
9
|
+
|
|
10
|
+
## 0.2.9 - 2026-03-16
|
|
11
|
+
|
|
12
|
+
- Keep startup checks cache-first and non-blocking.
|
|
13
|
+
- Add a one-time background live check per run.
|
|
14
|
+
- Show update prompt in the same session when the background check finds a newer version.
|
|
15
|
+
- Respect `PI_SKIP_VERSION_CHECK` and `PI_OFFLINE` for automatic checks.
|
|
16
|
+
- Avoid duplicate automatic prompts for the same version in one run.
|
|
17
|
+
- `/update` now warns and exits early when `PI_OFFLINE` is set.
|
package/README.md
CHANGED
|
@@ -1,21 +1,35 @@
|
|
|
1
1
|
# pi-updater
|
|
2
2
|
|
|
3
|
-
A Codex-style auto-updater for
|
|
3
|
+
A lightweight, Codex-style auto-updater for pi with fast, cache-first startup checks.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- npm: https://www.npmjs.com/package/pi-updater
|
|
6
|
+
- repo: https://github.com/tonze/pi-updater
|
|
7
|
+
|
|
8
|
+
> **Note:** Automatic installation currently supports npm-based pi installs only.
|
|
6
9
|
|
|
7
10
|
<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
11
|
|
|
9
12
|
## What it does
|
|
10
13
|
|
|
11
14
|
**On startup:** if a newer version is available, shows a prompt:
|
|
12
|
-
- **Update now** — install with npm, then restart pi
|
|
15
|
+
- **Update now** — install with npm, then auto-restart pi on the current session
|
|
13
16
|
- **Skip** — dismiss until next session
|
|
14
17
|
- **Skip this version** — don't ask again until a newer version appears
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
After a successful update, pi-updater asks whether to restart immediately. If confirmed, pi relaunches seamlessly on the current session. In non-interactive modes or if auto-restart fails, it falls back to a manual restart message.
|
|
20
|
+
|
|
21
|
+
**In the background (once per run):** performs one live npm check and can show the prompt in the same session when a new release is detected.
|
|
22
|
+
|
|
23
|
+
**`/update`:** manually check for updates (always fetches fresh from npm, unless `PI_OFFLINE` is set).
|
|
24
|
+
|
|
25
|
+
## How version checks work
|
|
17
26
|
|
|
18
|
-
|
|
27
|
+
pi-updater uses a cache-first approach to keep startup fast:
|
|
28
|
+
|
|
29
|
+
1. On startup, cached version data is checked instantly.
|
|
30
|
+
2. One background live fetch refreshes the cache.
|
|
31
|
+
3. If the background fetch finds a newer version, pi-updater can prompt in the same session.
|
|
32
|
+
4. Automatic checks are skipped when `PI_SKIP_VERSION_CHECK` or `PI_OFFLINE` is set.
|
|
19
33
|
|
|
20
34
|
## Install
|
|
21
35
|
|
|
@@ -33,6 +47,20 @@ pi install git:github.com/tonze/pi-updater
|
|
|
33
47
|
|
|
34
48
|
Use `/update` inside pi to manually check for updates and install them.
|
|
35
49
|
|
|
50
|
+
## Environment flags
|
|
51
|
+
|
|
52
|
+
Disable automatic version checks:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
export PI_SKIP_VERSION_CHECK=1
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or run in offline mode (also disables automatic checks):
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
export PI_OFFLINE=1
|
|
62
|
+
```
|
|
63
|
+
|
|
36
64
|
## Updating this package
|
|
37
65
|
|
|
38
66
|
```bash
|
package/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ExtensionContext,
|
|
4
4
|
} from "@mariozechner/pi-coding-agent";
|
|
5
5
|
import { VERSION, BorderedLoader } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
6
7
|
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
7
8
|
import { join, dirname } from "node:path";
|
|
8
9
|
import { homedir } from "node:os";
|
|
@@ -11,9 +12,13 @@ const PACKAGE_NAME = "@mariozechner/pi-coding-agent";
|
|
|
11
12
|
const REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
12
13
|
const CACHE_FILE = join(homedir(), ".pi", "agent", "update-cache.json");
|
|
13
14
|
|
|
15
|
+
const ENV_SKIP_VERSION_CHECK = "PI_SKIP_VERSION_CHECK";
|
|
16
|
+
const ENV_OFFLINE = "PI_OFFLINE";
|
|
17
|
+
|
|
14
18
|
interface VersionCache {
|
|
15
19
|
latestVersion: string;
|
|
16
20
|
dismissedVersion?: string;
|
|
21
|
+
checkedAt?: string;
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
function readCache(): VersionCache | undefined {
|
|
@@ -48,6 +53,27 @@ function isNewer(latest: string, current: string): boolean {
|
|
|
48
53
|
return l[2] > c[2];
|
|
49
54
|
}
|
|
50
55
|
|
|
56
|
+
function isEnvSet(name: string): boolean {
|
|
57
|
+
return Boolean(process.env[name]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function shouldSkipAutoChecks(): boolean {
|
|
61
|
+
return isEnvSet(ENV_SKIP_VERSION_CHECK) || isEnvSet(ENV_OFFLINE);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isOffline(): boolean {
|
|
65
|
+
return isEnvSet(ENV_OFFLINE);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function saveLatestToCache(latest: string) {
|
|
69
|
+
const prev = readCache();
|
|
70
|
+
writeCache({
|
|
71
|
+
latestVersion: latest,
|
|
72
|
+
dismissedVersion: prev?.dismissedVersion,
|
|
73
|
+
checkedAt: new Date().toISOString(),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
51
77
|
async function fetchLatestVersion(): Promise<string | undefined> {
|
|
52
78
|
try {
|
|
53
79
|
const res = await fetch(REGISTRY_URL, {
|
|
@@ -60,33 +86,30 @@ async function fetchLatestVersion(): Promise<string | undefined> {
|
|
|
60
86
|
}
|
|
61
87
|
}
|
|
62
88
|
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
* Always kicks off a background fetch to refresh the cache for the next run.
|
|
66
|
-
*/
|
|
67
|
-
function getUpgradeVersion(): string | undefined {
|
|
89
|
+
/** Returns a cached upgrade if available and not dismissed. */
|
|
90
|
+
function getCachedUpgradeVersion(): string | undefined {
|
|
68
91
|
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
92
|
if (!cache) return undefined;
|
|
80
93
|
if (!isNewer(cache.latestVersion, VERSION)) return undefined;
|
|
81
94
|
if (cache.dismissedVersion === cache.latestVersion) return undefined;
|
|
82
95
|
return cache.latestVersion;
|
|
83
96
|
}
|
|
84
97
|
|
|
98
|
+
/** Fetch latest from npm and refresh cache. */
|
|
99
|
+
async function refreshLatestVersionInCache(): Promise<string | undefined> {
|
|
100
|
+
const latest = await fetchLatestVersion();
|
|
101
|
+
if (!latest) return undefined;
|
|
102
|
+
saveLatestToCache(latest);
|
|
103
|
+
return latest;
|
|
104
|
+
}
|
|
105
|
+
|
|
85
106
|
function dismissVersion(version: string) {
|
|
86
107
|
const cache = readCache();
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
108
|
+
writeCache({
|
|
109
|
+
latestVersion: cache?.latestVersion ?? version,
|
|
110
|
+
dismissedVersion: version,
|
|
111
|
+
checkedAt: cache?.checkedAt,
|
|
112
|
+
});
|
|
90
113
|
}
|
|
91
114
|
|
|
92
115
|
function getInstallCommand(version: string): { program: string; args: string[] } {
|
|
@@ -101,6 +124,44 @@ function fmtCmd(cmd: { program: string; args: string[] }): string {
|
|
|
101
124
|
}
|
|
102
125
|
|
|
103
126
|
export default function (pi: ExtensionAPI) {
|
|
127
|
+
let promptOpen = false;
|
|
128
|
+
const promptedVersions = new Set<string>();
|
|
129
|
+
let liveCheckStarted = false;
|
|
130
|
+
|
|
131
|
+
async function findPiBinary(): Promise<string> {
|
|
132
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
133
|
+
const result = await pi.exec(cmd, ["pi"]);
|
|
134
|
+
if (result.code === 0 && result.stdout?.trim()) {
|
|
135
|
+
return result.stdout.trim().split(/\r?\n/)[0];
|
|
136
|
+
}
|
|
137
|
+
return "pi";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function canAutoRestart(ctx: ExtensionContext): boolean {
|
|
141
|
+
return ctx.hasUI && !!process.stdin.isTTY && !!process.stdout.isTTY;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function restartPi(ctx: ExtensionContext): Promise<boolean> {
|
|
145
|
+
const piBinary = await findPiBinary();
|
|
146
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
147
|
+
const restartArgs = sessionFile ? ["--session", sessionFile] : ["-c"];
|
|
148
|
+
|
|
149
|
+
return ctx.ui.custom<boolean>((tui, _theme, _kb, done) => {
|
|
150
|
+
tui.stop();
|
|
151
|
+
const result = spawnSync(piBinary, restartArgs, {
|
|
152
|
+
cwd: ctx.cwd,
|
|
153
|
+
env: process.env,
|
|
154
|
+
stdio: "inherit",
|
|
155
|
+
shell: process.platform === "win32",
|
|
156
|
+
windowsHide: false,
|
|
157
|
+
});
|
|
158
|
+
tui.start();
|
|
159
|
+
tui.requestRender(true);
|
|
160
|
+
done(!result.error && (result.status === null || result.status === 0));
|
|
161
|
+
return { render: () => [], invalidate: () => {} };
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
104
165
|
async function doInstall(
|
|
105
166
|
ctx: ExtensionContext,
|
|
106
167
|
latest: string,
|
|
@@ -110,37 +171,50 @@ export default function (pi: ExtensionAPI) {
|
|
|
110
171
|
const loader = new BorderedLoader(tui, theme, `Installing ${latest}...`);
|
|
111
172
|
loader.onAbort = () => done(false);
|
|
112
173
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
);
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
return true;
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
run()
|
|
132
|
-
.then(done)
|
|
174
|
+
pi.exec(cmd.program, cmd.args, { timeout: 120_000 })
|
|
175
|
+
.then((result) => {
|
|
176
|
+
if (result.code !== 0) {
|
|
177
|
+
ctx.ui.notify(
|
|
178
|
+
`Update failed (exit ${result.code}): ${result.stderr || result.stdout}`,
|
|
179
|
+
"error",
|
|
180
|
+
);
|
|
181
|
+
done(false);
|
|
182
|
+
} else {
|
|
183
|
+
done(true);
|
|
184
|
+
}
|
|
185
|
+
})
|
|
133
186
|
.catch(() => done(false));
|
|
187
|
+
|
|
134
188
|
return loader;
|
|
135
189
|
});
|
|
136
190
|
|
|
137
191
|
if (!success) return;
|
|
138
192
|
|
|
139
|
-
|
|
193
|
+
if (!canAutoRestart(ctx)) {
|
|
194
|
+
ctx.ui.notify(
|
|
195
|
+
`Updated to ${latest}! Please restart pi.\nTip: run \`pi -c\` to continue this session.`,
|
|
196
|
+
"info",
|
|
197
|
+
);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const restart = await ctx.ui.confirm(
|
|
140
202
|
`Updated to ${latest}!`,
|
|
141
|
-
"
|
|
203
|
+
"Restart now?",
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
if (!restart) return;
|
|
207
|
+
|
|
208
|
+
const ok = await restartPi(ctx);
|
|
209
|
+
if (ok) {
|
|
210
|
+
ctx.shutdown();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
ctx.ui.notify(
|
|
215
|
+
`Updated to ${latest}! Auto-restart failed. Please restart pi manually.\nTip: run \`pi -c\` to continue this session.`,
|
|
216
|
+
"error",
|
|
142
217
|
);
|
|
143
|
-
if (ok) ctx.shutdown();
|
|
144
218
|
}
|
|
145
219
|
|
|
146
220
|
async function showUpdatePrompt(ctx: ExtensionContext, latest: string) {
|
|
@@ -159,21 +233,96 @@ export default function (pi: ExtensionAPI) {
|
|
|
159
233
|
await doInstall(ctx, latest, cmd);
|
|
160
234
|
}
|
|
161
235
|
|
|
162
|
-
|
|
236
|
+
function canAutoPromptVersion(latest: string): boolean {
|
|
237
|
+
if (!isNewer(latest, VERSION)) return false;
|
|
238
|
+
if (promptedVersions.has(latest)) return false;
|
|
239
|
+
if (readCache()?.dismissedVersion === latest) return false;
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function maybeShowAutoPrompt(ctx: ExtensionContext, latest: string) {
|
|
163
244
|
if (!ctx.hasUI) return;
|
|
164
|
-
|
|
165
|
-
if (latest)
|
|
245
|
+
if (promptOpen) return;
|
|
246
|
+
if (!canAutoPromptVersion(latest)) return;
|
|
247
|
+
|
|
248
|
+
promptOpen = true;
|
|
249
|
+
promptedVersions.add(latest);
|
|
250
|
+
try {
|
|
251
|
+
await showUpdatePrompt(ctx, latest);
|
|
252
|
+
} finally {
|
|
253
|
+
promptOpen = false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function runAutoChecks(ctx: ExtensionContext) {
|
|
258
|
+
if (!ctx.hasUI) return;
|
|
259
|
+
if (shouldSkipAutoChecks()) return;
|
|
260
|
+
|
|
261
|
+
const cached = getCachedUpgradeVersion();
|
|
262
|
+
if (cached) void maybeShowAutoPrompt(ctx, cached);
|
|
263
|
+
|
|
264
|
+
if (liveCheckStarted) return;
|
|
265
|
+
liveCheckStarted = true;
|
|
266
|
+
|
|
267
|
+
void refreshLatestVersionInCache()
|
|
268
|
+
.then((latest) => {
|
|
269
|
+
if (!latest) return;
|
|
270
|
+
void maybeShowAutoPrompt(ctx, latest);
|
|
271
|
+
})
|
|
272
|
+
.catch(() => {});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
276
|
+
runAutoChecks(ctx);
|
|
166
277
|
});
|
|
167
278
|
|
|
168
279
|
pi.on("session_switch", async (_event, ctx) => {
|
|
169
|
-
|
|
170
|
-
const latest = getUpgradeVersion();
|
|
171
|
-
if (latest) void showUpdatePrompt(ctx, latest);
|
|
280
|
+
runAutoChecks(ctx);
|
|
172
281
|
});
|
|
173
282
|
|
|
174
283
|
pi.registerCommand("update", {
|
|
175
284
|
description: "Check for pi updates and install",
|
|
176
|
-
handler: async (
|
|
285
|
+
handler: async (rawArgs, ctx) => {
|
|
286
|
+
// /update --test — simulate the full UI flow without a real install
|
|
287
|
+
if (rawArgs?.trim() === "--test") {
|
|
288
|
+
const fakeLatest = "99.0.0";
|
|
289
|
+
const cmd = getInstallCommand(fakeLatest);
|
|
290
|
+
const choice = await ctx.ui.select(`Update ${VERSION} → ${fakeLatest}`, [
|
|
291
|
+
`Update now (${fmtCmd(cmd)})`,
|
|
292
|
+
"Skip",
|
|
293
|
+
"Skip this version",
|
|
294
|
+
]);
|
|
295
|
+
if (!choice || choice === "Skip" || choice === "Skip this version") return;
|
|
296
|
+
|
|
297
|
+
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
298
|
+
const loader = new BorderedLoader(tui, theme, `Installing ${fakeLatest}...`);
|
|
299
|
+
loader.onAbort = () => done();
|
|
300
|
+
setTimeout(() => done(), 1500);
|
|
301
|
+
return loader;
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (!canAutoRestart(ctx)) {
|
|
305
|
+
ctx.ui.notify(`Updated to ${fakeLatest}! Please restart pi.`, "info");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const restart = await ctx.ui.confirm(`Updated to ${fakeLatest}!`, "Restart now?");
|
|
310
|
+
if (!restart) return;
|
|
311
|
+
|
|
312
|
+
const ok = await restartPi(ctx);
|
|
313
|
+
if (ok) { ctx.shutdown(); return; }
|
|
314
|
+
ctx.ui.notify("Test restart failed.", "error");
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (isOffline()) {
|
|
319
|
+
ctx.ui.notify(
|
|
320
|
+
"PI_OFFLINE is set. Disable it to check for updates.",
|
|
321
|
+
"warning",
|
|
322
|
+
);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
177
326
|
const latest = await ctx.ui.custom<string | null>(
|
|
178
327
|
(tui, theme, _kb, done) => {
|
|
179
328
|
const loader = new BorderedLoader(
|
|
@@ -194,16 +343,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
194
343
|
return;
|
|
195
344
|
}
|
|
196
345
|
|
|
197
|
-
|
|
198
|
-
latestVersion: latest,
|
|
199
|
-
dismissedVersion: readCache()?.dismissedVersion,
|
|
200
|
-
});
|
|
346
|
+
saveLatestToCache(latest);
|
|
201
347
|
|
|
202
348
|
if (!isNewer(latest, VERSION)) {
|
|
203
349
|
ctx.ui.notify(`Already on latest version (${VERSION}).`, "info");
|
|
204
350
|
return;
|
|
205
351
|
}
|
|
206
352
|
|
|
353
|
+
promptedVersions.add(latest);
|
|
207
354
|
await showUpdatePrompt(ctx, latest);
|
|
208
355
|
},
|
|
209
356
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-updater",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Codex-style auto-updater for pi. Checks for new versions on startup and prompts to install.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"index.ts",
|
|
25
|
-
"README.md"
|
|
25
|
+
"README.md",
|
|
26
|
+
"CHANGELOG.md"
|
|
26
27
|
],
|
|
27
28
|
"peerDependencies": {
|
|
28
29
|
"@mariozechner/pi-coding-agent": "*"
|