pi-updater 0.3.1 → 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 +6 -2
- package/README.md +6 -6
- package/index.ts +128 -29
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,13 +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
|
+
|
|
3
9
|
## 0.3.1 - 2026-04-04
|
|
4
10
|
|
|
5
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).
|
|
6
12
|
- Store cache and dismissed-version state in pi's configured agent directory.
|
|
7
13
|
- Preserve `--no-session` mode when restarting after an update and show the correct manual restart hint.
|
|
8
14
|
|
|
9
|
-
## Unreleased
|
|
10
|
-
|
|
11
15
|
## 0.3.0 - 2026-03-23
|
|
12
16
|
|
|
13
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
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.
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
**`/update`:** manually check for updates (always fetches fresh from npm, unless `PI_OFFLINE` is set).
|
|
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
|
|
package/index.ts
CHANGED
|
@@ -8,11 +8,13 @@ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
|
8
8
|
import { join, dirname } from "node:path";
|
|
9
9
|
|
|
10
10
|
const PACKAGE_NAME = "@mariozechner/pi-coding-agent";
|
|
11
|
-
const
|
|
11
|
+
const LATEST_VERSION_URL = "https://pi.dev/api/latest-version";
|
|
12
|
+
const NATIVE_SELF_UPDATE_MIN_VERSION = "0.70.3";
|
|
12
13
|
const CACHE_FILE = join(getAgentDir(), "update-cache.json");
|
|
13
14
|
|
|
14
15
|
const ENV_SKIP_VERSION_CHECK = "PI_SKIP_VERSION_CHECK";
|
|
15
16
|
const ENV_OFFLINE = "PI_OFFLINE";
|
|
17
|
+
const ENV_INTERNAL_SKIP = "PI_UPDATER_SUPPRESSED_NATIVE_VERSION_CHECK";
|
|
16
18
|
|
|
17
19
|
interface VersionCache {
|
|
18
20
|
latestVersion: string;
|
|
@@ -35,35 +37,75 @@ function writeCache(cache: VersionCache) {
|
|
|
35
37
|
} catch {}
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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);
|
|
44
71
|
}
|
|
45
72
|
|
|
46
73
|
function isNewer(latest: string, current: string): boolean {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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;
|
|
53
81
|
}
|
|
54
82
|
|
|
55
83
|
function isEnvSet(name: string): boolean {
|
|
56
84
|
return Boolean(process.env[name]);
|
|
57
85
|
}
|
|
58
86
|
|
|
87
|
+
const userSkippedVersionCheck =
|
|
88
|
+
isEnvSet(ENV_SKIP_VERSION_CHECK) && !isEnvSet(ENV_INTERNAL_SKIP);
|
|
89
|
+
|
|
59
90
|
function shouldSkipAutoChecks(): boolean {
|
|
60
|
-
return
|
|
91
|
+
return userSkippedVersionCheck || isEnvSet(ENV_OFFLINE);
|
|
61
92
|
}
|
|
62
93
|
|
|
63
94
|
function isOffline(): boolean {
|
|
64
95
|
return isEnvSet(ENV_OFFLINE);
|
|
65
96
|
}
|
|
66
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
|
+
|
|
67
109
|
function saveLatestToCache(latest: string) {
|
|
68
110
|
const prev = readCache();
|
|
69
111
|
writeCache({
|
|
@@ -75,11 +117,18 @@ function saveLatestToCache(latest: string) {
|
|
|
75
117
|
|
|
76
118
|
async function fetchLatestVersion(): Promise<string | undefined> {
|
|
77
119
|
try {
|
|
78
|
-
const res = await fetch(
|
|
120
|
+
const res = await fetch(LATEST_VERSION_URL, {
|
|
121
|
+
headers: {
|
|
122
|
+
"User-Agent": piUserAgent(),
|
|
123
|
+
accept: "application/json",
|
|
124
|
+
},
|
|
79
125
|
signal: AbortSignal.timeout(10_000),
|
|
80
126
|
});
|
|
81
127
|
if (!res.ok) return undefined;
|
|
82
|
-
|
|
128
|
+
const version = ((await res.json()) as { version?: string }).version;
|
|
129
|
+
return typeof version === "string" && version.trim()
|
|
130
|
+
? version.trim()
|
|
131
|
+
: undefined;
|
|
83
132
|
} catch {
|
|
84
133
|
return undefined;
|
|
85
134
|
}
|
|
@@ -94,7 +143,7 @@ function getCachedUpgradeVersion(): string | undefined {
|
|
|
94
143
|
return cache.latestVersion;
|
|
95
144
|
}
|
|
96
145
|
|
|
97
|
-
/** Fetch latest from
|
|
146
|
+
/** Fetch latest from Pi's update endpoint and refresh cache. */
|
|
98
147
|
async function refreshLatestVersionInCache(): Promise<string | undefined> {
|
|
99
148
|
const latest = await fetchLatestVersion();
|
|
100
149
|
if (!latest) return undefined;
|
|
@@ -111,18 +160,39 @@ function dismissVersion(version: string) {
|
|
|
111
160
|
});
|
|
112
161
|
}
|
|
113
162
|
|
|
114
|
-
|
|
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
|
+
|
|
115
178
|
return {
|
|
116
179
|
program: "npm",
|
|
117
180
|
args: ["install", "-g", `${PACKAGE_NAME}@${version}`],
|
|
181
|
+
display: `npm install -g ${PACKAGE_NAME}@${version}`,
|
|
118
182
|
};
|
|
119
183
|
}
|
|
120
184
|
|
|
121
|
-
function fmtCmd(cmd:
|
|
122
|
-
return
|
|
185
|
+
function fmtCmd(cmd: InstallCommand): string {
|
|
186
|
+
return cmd.display;
|
|
123
187
|
}
|
|
124
188
|
|
|
125
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
|
+
|
|
126
196
|
let promptOpen = false;
|
|
127
197
|
const promptedVersions = new Set<string>();
|
|
128
198
|
let liveCheckStarted = false;
|
|
@@ -144,12 +214,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
144
214
|
const piBinary = await findPiBinary();
|
|
145
215
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
146
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
|
+
}
|
|
147
222
|
|
|
148
223
|
return ctx.ui.custom<boolean>((tui, _theme, _kb, done) => {
|
|
149
224
|
tui.stop();
|
|
150
225
|
const result = spawnSync(piBinary, restartArgs, {
|
|
151
226
|
cwd: ctx.cwd,
|
|
152
|
-
env
|
|
227
|
+
env,
|
|
153
228
|
stdio: "inherit",
|
|
154
229
|
shell: process.platform === "win32",
|
|
155
230
|
windowsHide: false,
|
|
@@ -164,17 +239,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
164
239
|
async function doInstall(
|
|
165
240
|
ctx: ExtensionContext,
|
|
166
241
|
latest: string,
|
|
167
|
-
cmd:
|
|
242
|
+
cmd: InstallCommand,
|
|
168
243
|
) {
|
|
169
244
|
const success = await ctx.ui.custom<boolean>((tui, theme, _kb, done) => {
|
|
170
|
-
const loader = new BorderedLoader(tui, theme, `
|
|
245
|
+
const loader = new BorderedLoader(tui, theme, `Running ${cmd.display}...`);
|
|
171
246
|
loader.onAbort = () => done(false);
|
|
172
247
|
|
|
173
|
-
|
|
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()
|
|
174
263
|
.then((result) => {
|
|
175
264
|
if (result.code !== 0) {
|
|
265
|
+
const output = [result.stderr, result.stdout]
|
|
266
|
+
.filter(Boolean)
|
|
267
|
+
.join("\n")
|
|
268
|
+
.trim();
|
|
176
269
|
ctx.ui.notify(
|
|
177
|
-
`Update failed (exit ${result.code})
|
|
270
|
+
`Update failed (exit ${result.code})${output ? `: ${output}` : ""}`,
|
|
178
271
|
"error",
|
|
179
272
|
);
|
|
180
273
|
done(false);
|
|
@@ -182,7 +275,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
182
275
|
done(true);
|
|
183
276
|
}
|
|
184
277
|
})
|
|
185
|
-
.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
|
+
});
|
|
186
285
|
|
|
187
286
|
return loader;
|
|
188
287
|
});
|
|
@@ -281,7 +380,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
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
|
|