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 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:** 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
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
- **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).
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 REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
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
- function parseVersion(v: string): [number, number, number] | undefined {
39
- const parts = v.trim().split(".");
40
- if (parts.length !== 3) return undefined;
41
- const nums = parts.map(Number);
42
- if (nums.some(isNaN)) return undefined;
43
- 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);
44
71
  }
45
72
 
46
73
  function isNewer(latest: string, current: string): boolean {
47
- const l = parseVersion(latest);
48
- const c = parseVersion(current);
49
- if (!l || !c) return false;
50
- if (l[0] !== c[0]) return l[0] > c[0];
51
- if (l[1] !== c[1]) return l[1] > c[1];
52
- 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;
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 isEnvSet(ENV_SKIP_VERSION_CHECK) || isEnvSet(ENV_OFFLINE);
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(REGISTRY_URL, {
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
- 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;
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 npm and refresh cache. */
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
- 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
+
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: { program: string; args: string[] }): string {
122
- return `${cmd.program} ${cmd.args.join(" ")}`;
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: process.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: { program: string; args: string[] },
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, `Installing ${latest}...`);
245
+ const loader = new BorderedLoader(tui, theme, `Running ${cmd.display}...`);
171
246
  loader.onAbort = () => done(false);
172
247
 
173
- 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()
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}): ${result.stderr || result.stdout}`,
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(() => 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
+ });
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, `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.1",
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",