pi-updater 0.3.0 → 0.3.2
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 +12 -0
- package/README.md +9 -7
- package/index.ts +139 -40
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.2 - 2026-05-02
|
|
4
|
+
|
|
5
|
+
- Use pi's native `pi update --self` installer on pi 0.70.3+ and keep npm install as the fallback for older pi versions.
|
|
6
|
+
- Use pi's `https://pi.dev/api/latest-version` update endpoint with a `pi/<version>` user agent.
|
|
7
|
+
- Keep pi-updater's interactive startup prompt on pi 0.70.3+ while avoiding pi's duplicate built-in version notice.
|
|
8
|
+
|
|
9
|
+
## 0.3.1 - 2026-04-04
|
|
10
|
+
|
|
11
|
+
- Compatibility with pi 0.65+: use `session_start` instead of legacy `session_switch` for automatic checks. See [pi-mono v0.65.0](https://github.com/badlogic/pi-mono/releases/tag/v0.65.0).
|
|
12
|
+
- Store cache and dismissed-version state in pi's configured agent directory.
|
|
13
|
+
- Preserve `--no-session` mode when restarting after an update and show the correct manual restart hint.
|
|
14
|
+
|
|
3
15
|
## 0.3.0 - 2026-03-23
|
|
4
16
|
|
|
5
17
|
- Auto-restart pi after a successful update. Asks to restart, then seamlessly relaunches on the current session.
|
package/README.md
CHANGED
|
@@ -5,29 +5,29 @@ A lightweight, Codex-style auto-updater for pi with fast, cache-first startup ch
|
|
|
5
5
|
- npm: https://www.npmjs.com/package/pi-updater
|
|
6
6
|
- repo: https://github.com/tonze/pi-updater
|
|
7
7
|
|
|
8
|
-
> **Note:**
|
|
8
|
+
> **Note:** On pi 0.70.3+, pi-updater delegates installation to pi's native `pi update --self` command. Older pi versions fall back to npm-based installation.
|
|
9
9
|
|
|
10
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" />
|
|
11
11
|
|
|
12
12
|
## What it does
|
|
13
13
|
|
|
14
|
-
**On startup
|
|
14
|
+
**On pi 0.70.3+:** pi-updater is native-aware. It keeps the interactive startup update prompt, checks pi's update service, installs with `pi update --self`, then offers to restart the current session. Pi's built-in updater only shows a notice with a command to run; pi-updater provides the clickable update/restart flow.
|
|
15
|
+
|
|
16
|
+
**On older pi versions:** if a newer version is available, pi-updater shows a startup prompt:
|
|
15
17
|
- **Update now** — install with npm, then auto-restart pi on the current session
|
|
16
18
|
- **Skip** — dismiss until next session
|
|
17
19
|
- **Skip this version** — don't ask again until a newer version appears
|
|
18
20
|
|
|
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.
|
|
21
|
+
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. Ephemeral `--no-session` runs stay ephemeral on restart.
|
|
22
22
|
|
|
23
|
-
**`/update`:** manually check for updates (always fetches fresh from
|
|
23
|
+
**`/update`:** manually check for updates (always fetches fresh from pi's update service, unless `PI_OFFLINE` is set). On pi 0.70.3+ it installs with `pi update --self`; on older pi it falls back to npm.
|
|
24
24
|
|
|
25
25
|
## How version checks work
|
|
26
26
|
|
|
27
27
|
pi-updater uses a cache-first approach to keep startup fast:
|
|
28
28
|
|
|
29
29
|
1. On startup, cached version data is checked instantly.
|
|
30
|
-
2. One background live fetch refreshes the cache.
|
|
30
|
+
2. One background live fetch refreshes the cache from pi's update service.
|
|
31
31
|
3. If the background fetch finds a newer version, pi-updater can prompt in the same session.
|
|
32
32
|
4. Automatic checks are skipped when `PI_SKIP_VERSION_CHECK` or `PI_OFFLINE` is set.
|
|
33
33
|
|
|
@@ -47,6 +47,8 @@ pi install git:github.com/tonze/pi-updater
|
|
|
47
47
|
|
|
48
48
|
Use `/update` inside pi to manually check for updates and install them.
|
|
49
49
|
|
|
50
|
+
Cache and dismissed-version state are stored in pi's configured agent directory and respect `PI_CODING_AGENT_DIR`.
|
|
51
|
+
|
|
50
52
|
## Environment flags
|
|
51
53
|
|
|
52
54
|
Disable automatic version checks:
|
package/index.ts
CHANGED
|
@@ -2,18 +2,19 @@ import type {
|
|
|
2
2
|
ExtensionAPI,
|
|
3
3
|
ExtensionContext,
|
|
4
4
|
} from "@mariozechner/pi-coding-agent";
|
|
5
|
-
import { VERSION, BorderedLoader } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
import { VERSION, BorderedLoader, getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
8
8
|
import { join, dirname } from "node:path";
|
|
9
|
-
import { homedir } from "node:os";
|
|
10
9
|
|
|
11
10
|
const PACKAGE_NAME = "@mariozechner/pi-coding-agent";
|
|
12
|
-
const
|
|
13
|
-
const
|
|
11
|
+
const LATEST_VERSION_URL = "https://pi.dev/api/latest-version";
|
|
12
|
+
const NATIVE_SELF_UPDATE_MIN_VERSION = "0.70.3";
|
|
13
|
+
const CACHE_FILE = join(getAgentDir(), "update-cache.json");
|
|
14
14
|
|
|
15
15
|
const ENV_SKIP_VERSION_CHECK = "PI_SKIP_VERSION_CHECK";
|
|
16
16
|
const ENV_OFFLINE = "PI_OFFLINE";
|
|
17
|
+
const ENV_INTERNAL_SKIP = "PI_UPDATER_SUPPRESSED_NATIVE_VERSION_CHECK";
|
|
17
18
|
|
|
18
19
|
interface VersionCache {
|
|
19
20
|
latestVersion: string;
|
|
@@ -36,35 +37,75 @@ function writeCache(cache: VersionCache) {
|
|
|
36
37
|
} catch {}
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
interface ParsedVersion {
|
|
41
|
+
major: number;
|
|
42
|
+
minor: number;
|
|
43
|
+
patch: number;
|
|
44
|
+
prerelease?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseVersion(version: string): ParsedVersion | undefined {
|
|
48
|
+
const match = version
|
|
49
|
+
.trim()
|
|
50
|
+
.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$/);
|
|
51
|
+
if (!match) return undefined;
|
|
52
|
+
return {
|
|
53
|
+
major: Number.parseInt(match[1], 10),
|
|
54
|
+
minor: Number.parseInt(match[2], 10),
|
|
55
|
+
patch: Number.parseInt(match[3], 10),
|
|
56
|
+
prerelease: match[4],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function compareVersions(leftVersion: string, rightVersion: string): number | undefined {
|
|
61
|
+
const left = parseVersion(leftVersion);
|
|
62
|
+
const right = parseVersion(rightVersion);
|
|
63
|
+
if (!left || !right) return undefined;
|
|
64
|
+
if (left.major !== right.major) return left.major - right.major;
|
|
65
|
+
if (left.minor !== right.minor) return left.minor - right.minor;
|
|
66
|
+
if (left.patch !== right.patch) return left.patch - right.patch;
|
|
67
|
+
if (left.prerelease === right.prerelease) return 0;
|
|
68
|
+
if (!left.prerelease) return 1;
|
|
69
|
+
if (!right.prerelease) return -1;
|
|
70
|
+
return left.prerelease.localeCompare(right.prerelease);
|
|
45
71
|
}
|
|
46
72
|
|
|
47
73
|
function isNewer(latest: string, current: string): boolean {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
74
|
+
const comparison = compareVersions(latest, current);
|
|
75
|
+
return comparison !== undefined && comparison > 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isAtLeast(version: string, minimum: string): boolean {
|
|
79
|
+
const comparison = compareVersions(version, minimum);
|
|
80
|
+
return comparison !== undefined && comparison >= 0;
|
|
54
81
|
}
|
|
55
82
|
|
|
56
83
|
function isEnvSet(name: string): boolean {
|
|
57
84
|
return Boolean(process.env[name]);
|
|
58
85
|
}
|
|
59
86
|
|
|
87
|
+
const userSkippedVersionCheck =
|
|
88
|
+
isEnvSet(ENV_SKIP_VERSION_CHECK) && !isEnvSet(ENV_INTERNAL_SKIP);
|
|
89
|
+
|
|
60
90
|
function shouldSkipAutoChecks(): boolean {
|
|
61
|
-
return
|
|
91
|
+
return userSkippedVersionCheck || isEnvSet(ENV_OFFLINE);
|
|
62
92
|
}
|
|
63
93
|
|
|
64
94
|
function isOffline(): boolean {
|
|
65
95
|
return isEnvSet(ENV_OFFLINE);
|
|
66
96
|
}
|
|
67
97
|
|
|
98
|
+
function piUserAgent(): string {
|
|
99
|
+
const runtime = process.versions.bun
|
|
100
|
+
? `bun/${process.versions.bun}`
|
|
101
|
+
: `node/${process.version}`;
|
|
102
|
+
return `pi/${VERSION} (${process.platform}; ${runtime}; ${process.arch})`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function hasNativeSelfUpdate(): boolean {
|
|
106
|
+
return isAtLeast(VERSION, NATIVE_SELF_UPDATE_MIN_VERSION);
|
|
107
|
+
}
|
|
108
|
+
|
|
68
109
|
function saveLatestToCache(latest: string) {
|
|
69
110
|
const prev = readCache();
|
|
70
111
|
writeCache({
|
|
@@ -76,11 +117,18 @@ function saveLatestToCache(latest: string) {
|
|
|
76
117
|
|
|
77
118
|
async function fetchLatestVersion(): Promise<string | undefined> {
|
|
78
119
|
try {
|
|
79
|
-
const res = await fetch(
|
|
120
|
+
const res = await fetch(LATEST_VERSION_URL, {
|
|
121
|
+
headers: {
|
|
122
|
+
"User-Agent": piUserAgent(),
|
|
123
|
+
accept: "application/json",
|
|
124
|
+
},
|
|
80
125
|
signal: AbortSignal.timeout(10_000),
|
|
81
126
|
});
|
|
82
127
|
if (!res.ok) return undefined;
|
|
83
|
-
|
|
128
|
+
const version = ((await res.json()) as { version?: string }).version;
|
|
129
|
+
return typeof version === "string" && version.trim()
|
|
130
|
+
? version.trim()
|
|
131
|
+
: undefined;
|
|
84
132
|
} catch {
|
|
85
133
|
return undefined;
|
|
86
134
|
}
|
|
@@ -95,7 +143,7 @@ function getCachedUpgradeVersion(): string | undefined {
|
|
|
95
143
|
return cache.latestVersion;
|
|
96
144
|
}
|
|
97
145
|
|
|
98
|
-
/** Fetch latest from
|
|
146
|
+
/** Fetch latest from Pi's update endpoint and refresh cache. */
|
|
99
147
|
async function refreshLatestVersionInCache(): Promise<string | undefined> {
|
|
100
148
|
const latest = await fetchLatestVersion();
|
|
101
149
|
if (!latest) return undefined;
|
|
@@ -112,18 +160,39 @@ function dismissVersion(version: string) {
|
|
|
112
160
|
});
|
|
113
161
|
}
|
|
114
162
|
|
|
115
|
-
|
|
163
|
+
interface InstallCommand {
|
|
164
|
+
program: string;
|
|
165
|
+
args: string[];
|
|
166
|
+
display: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function getInstallCommand(version: string): InstallCommand {
|
|
170
|
+
if (hasNativeSelfUpdate()) {
|
|
171
|
+
return {
|
|
172
|
+
program: "pi",
|
|
173
|
+
args: ["update", "--self"],
|
|
174
|
+
display: "pi update --self",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
116
178
|
return {
|
|
117
179
|
program: "npm",
|
|
118
180
|
args: ["install", "-g", `${PACKAGE_NAME}@${version}`],
|
|
181
|
+
display: `npm install -g ${PACKAGE_NAME}@${version}`,
|
|
119
182
|
};
|
|
120
183
|
}
|
|
121
184
|
|
|
122
|
-
function fmtCmd(cmd:
|
|
123
|
-
return
|
|
185
|
+
function fmtCmd(cmd: InstallCommand): string {
|
|
186
|
+
return cmd.display;
|
|
124
187
|
}
|
|
125
188
|
|
|
126
189
|
export default function (pi: ExtensionAPI) {
|
|
190
|
+
const suppressNativeCheck = hasNativeSelfUpdate() && !userSkippedVersionCheck;
|
|
191
|
+
if (suppressNativeCheck) {
|
|
192
|
+
process.env[ENV_SKIP_VERSION_CHECK] = "1";
|
|
193
|
+
process.env[ENV_INTERNAL_SKIP] = "1";
|
|
194
|
+
}
|
|
195
|
+
|
|
127
196
|
let promptOpen = false;
|
|
128
197
|
const promptedVersions = new Set<string>();
|
|
129
198
|
let liveCheckStarted = false;
|
|
@@ -144,13 +213,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
144
213
|
async function restartPi(ctx: ExtensionContext): Promise<boolean> {
|
|
145
214
|
const piBinary = await findPiBinary();
|
|
146
215
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
147
|
-
const restartArgs = sessionFile ? ["--session", sessionFile] : ["-
|
|
216
|
+
const restartArgs = sessionFile ? ["--session", sessionFile] : ["--no-session"];
|
|
217
|
+
const env = { ...process.env };
|
|
218
|
+
if (suppressNativeCheck) {
|
|
219
|
+
delete env[ENV_SKIP_VERSION_CHECK];
|
|
220
|
+
delete env[ENV_INTERNAL_SKIP];
|
|
221
|
+
}
|
|
148
222
|
|
|
149
223
|
return ctx.ui.custom<boolean>((tui, _theme, _kb, done) => {
|
|
150
224
|
tui.stop();
|
|
151
225
|
const result = spawnSync(piBinary, restartArgs, {
|
|
152
226
|
cwd: ctx.cwd,
|
|
153
|
-
env
|
|
227
|
+
env,
|
|
154
228
|
stdio: "inherit",
|
|
155
229
|
shell: process.platform === "win32",
|
|
156
230
|
windowsHide: false,
|
|
@@ -165,17 +239,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
165
239
|
async function doInstall(
|
|
166
240
|
ctx: ExtensionContext,
|
|
167
241
|
latest: string,
|
|
168
|
-
cmd:
|
|
242
|
+
cmd: InstallCommand,
|
|
169
243
|
) {
|
|
170
244
|
const success = await ctx.ui.custom<boolean>((tui, theme, _kb, done) => {
|
|
171
|
-
const loader = new BorderedLoader(tui, theme, `
|
|
245
|
+
const loader = new BorderedLoader(tui, theme, `Running ${cmd.display}...`);
|
|
172
246
|
loader.onAbort = () => done(false);
|
|
173
247
|
|
|
174
|
-
|
|
248
|
+
const runUpdateCommand = async () => {
|
|
249
|
+
if (suppressNativeCheck && cmd.program === "pi") {
|
|
250
|
+
delete process.env[ENV_SKIP_VERSION_CHECK];
|
|
251
|
+
delete process.env[ENV_INTERNAL_SKIP];
|
|
252
|
+
try {
|
|
253
|
+
return await pi.exec(cmd.program, cmd.args, { timeout: 120_000 });
|
|
254
|
+
} finally {
|
|
255
|
+
process.env[ENV_SKIP_VERSION_CHECK] = "1";
|
|
256
|
+
process.env[ENV_INTERNAL_SKIP] = "1";
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return pi.exec(cmd.program, cmd.args, { timeout: 120_000 });
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
runUpdateCommand()
|
|
175
263
|
.then((result) => {
|
|
176
264
|
if (result.code !== 0) {
|
|
265
|
+
const output = [result.stderr, result.stdout]
|
|
266
|
+
.filter(Boolean)
|
|
267
|
+
.join("\n")
|
|
268
|
+
.trim();
|
|
177
269
|
ctx.ui.notify(
|
|
178
|
-
`Update failed (exit ${result.code})
|
|
270
|
+
`Update failed (exit ${result.code})${output ? `: ${output}` : ""}`,
|
|
179
271
|
"error",
|
|
180
272
|
);
|
|
181
273
|
done(false);
|
|
@@ -183,16 +275,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
183
275
|
done(true);
|
|
184
276
|
}
|
|
185
277
|
})
|
|
186
|
-
.catch(() =>
|
|
278
|
+
.catch((error) => {
|
|
279
|
+
ctx.ui.notify(
|
|
280
|
+
`Update failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
281
|
+
"error",
|
|
282
|
+
);
|
|
283
|
+
done(false);
|
|
284
|
+
});
|
|
187
285
|
|
|
188
286
|
return loader;
|
|
189
287
|
});
|
|
190
288
|
|
|
191
289
|
if (!success) return;
|
|
192
290
|
|
|
291
|
+
const restartTip = ctx.sessionManager.getSessionFile()
|
|
292
|
+
? "Tip: run `pi -c` to continue this session."
|
|
293
|
+
: "Tip: run `pi --no-session` to continue without a saved session.";
|
|
294
|
+
|
|
193
295
|
if (!canAutoRestart(ctx)) {
|
|
194
296
|
ctx.ui.notify(
|
|
195
|
-
`Updated to ${latest}! Please restart pi.\
|
|
297
|
+
`Updated to ${latest}! Please restart pi.\n${restartTip}`,
|
|
196
298
|
"info",
|
|
197
299
|
);
|
|
198
300
|
return;
|
|
@@ -212,7 +314,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
212
314
|
}
|
|
213
315
|
|
|
214
316
|
ctx.ui.notify(
|
|
215
|
-
`Updated to ${latest}! Auto-restart failed. Please restart pi manually.\
|
|
317
|
+
`Updated to ${latest}! Auto-restart failed. Please restart pi manually.\n${restartTip}`,
|
|
216
318
|
"error",
|
|
217
319
|
);
|
|
218
320
|
}
|
|
@@ -272,16 +374,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
272
374
|
.catch(() => {});
|
|
273
375
|
}
|
|
274
376
|
|
|
275
|
-
pi.on("session_start", async (
|
|
276
|
-
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
pi.on("session_switch", async (_event, ctx) => {
|
|
377
|
+
pi.on("session_start", async (event, ctx) => {
|
|
378
|
+
if (event.reason === "reload" || event.reason === "fork") return;
|
|
280
379
|
runAutoChecks(ctx);
|
|
281
380
|
});
|
|
282
381
|
|
|
283
382
|
pi.registerCommand("update", {
|
|
284
|
-
description: "Check for pi updates and install",
|
|
383
|
+
description: "Check for pi updates and install via native updater when available",
|
|
285
384
|
handler: async (rawArgs, ctx) => {
|
|
286
385
|
// /update --test — simulate the full UI flow without a real install
|
|
287
386
|
if (rawArgs?.trim() === "--test") {
|
|
@@ -295,7 +394,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
295
394
|
if (!choice || choice === "Skip" || choice === "Skip this version") return;
|
|
296
395
|
|
|
297
396
|
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
298
|
-
const loader = new BorderedLoader(tui, theme, `
|
|
397
|
+
const loader = new BorderedLoader(tui, theme, `Running ${cmd.display}...`);
|
|
299
398
|
loader.onAbort = () => done();
|
|
300
399
|
setTimeout(() => done(), 1500);
|
|
301
400
|
return loader;
|
|
@@ -339,7 +438,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
339
438
|
);
|
|
340
439
|
|
|
341
440
|
if (!latest) {
|
|
342
|
-
ctx.ui.notify("Could not reach
|
|
441
|
+
ctx.ui.notify("Could not reach Pi update service.", "error");
|
|
343
442
|
return;
|
|
344
443
|
}
|
|
345
444
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-updater",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@mariozechner/pi-coding-agent": "*"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@mariozechner/pi-coding-agent": "^0.
|
|
32
|
+
"@mariozechner/pi-coding-agent": "^0.65.0",
|
|
33
33
|
"@types/node": "^25.3.2",
|
|
34
34
|
"typescript": "^5.9.3"
|
|
35
35
|
}
|