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 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:** Automatic installation currently supports npm-based pi installs only.
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:** if a newer version is available, shows a prompt:
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 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
 
@@ -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 REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
13
- const CACHE_FILE = join(homedir(), ".pi", "agent", "update-cache.json");
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
- function parseVersion(v: string): [number, number, number] | undefined {
40
- const parts = v.trim().split(".");
41
- if (parts.length !== 3) return undefined;
42
- const nums = parts.map(Number);
43
- if (nums.some(isNaN)) return undefined;
44
- return nums as [number, number, number];
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 l = parseVersion(latest);
49
- const c = parseVersion(current);
50
- if (!l || !c) return false;
51
- if (l[0] !== c[0]) return l[0] > c[0];
52
- if (l[1] !== c[1]) return l[1] > c[1];
53
- return l[2] > c[2];
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 isEnvSet(ENV_SKIP_VERSION_CHECK) || isEnvSet(ENV_OFFLINE);
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(REGISTRY_URL, {
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
- return ((await res.json()) as { version?: string }).version;
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 npm and refresh cache. */
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
- function getInstallCommand(version: string): { program: string; args: string[] } {
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: { program: string; args: string[] }): string {
123
- return `${cmd.program} ${cmd.args.join(" ")}`;
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] : ["-c"];
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: process.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: { program: string; args: string[] },
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, `Installing ${latest}...`);
245
+ const loader = new BorderedLoader(tui, theme, `Running ${cmd.display}...`);
172
246
  loader.onAbort = () => done(false);
173
247
 
174
- pi.exec(cmd.program, cmd.args, { timeout: 120_000 })
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}): ${result.stderr || result.stdout}`,
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(() => done(false));
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.\nTip: run \`pi -c\` to continue this session.`,
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.\nTip: run \`pi -c\` to continue this session.`,
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 (_event, ctx) => {
276
- runAutoChecks(ctx);
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, `Installing ${fakeLatest}...`);
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 npm registry.", "error");
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.0",
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.55.1",
32
+ "@mariozechner/pi-coding-agent": "^0.65.0",
33
33
  "@types/node": "^25.3.2",
34
34
  "typescript": "^5.9.3"
35
35
  }