pi-updater 0.3.2 → 0.3.3

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.
Files changed (4) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +3 -5
  3. package/index.ts +273 -102
  4. package/package.json +8 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.3.3 - 2026-05-21
6
+
7
+ - Honor the update service `packageName` and install the explicit advertised npm package/version.
8
+ - Avoid native `pi update --self` so pi-updater can update through stale native self-update behavior.
9
+ - Keep loading under the legacy `@mariozechner/pi-coding-agent` runtime so package-name migrations can run.
10
+ - Migrate the global pi binary when the update service advertises a new package name by force-installing the advertised package after an engine-strict dry run.
11
+ - Treat package-name-only migrations as updates only when the advertised version is unchanged or newer, while staying on the current package if `packageName` is absent.
12
+ - Respect npm engine requirements during pi installs so updates fail safely when Node.js is too old.
13
+ - Switch extension imports and optional peer dependency to `@earendil-works/pi-coding-agent` so installing pi-updater no longer pulls the old `@mariozechner` pi package.
14
+
3
15
  ## 0.3.2 - 2026-05-02
4
16
 
5
17
  - Use pi's native `pi update --self` installer on pi 0.70.3+ and keep npm install as the fallback for older pi versions.
package/README.md CHANGED
@@ -5,22 +5,20 @@ 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:** 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.
8
+ > **Note:** pi-updater installs the exact package/version returned by pi's update service with npm. This handles pi package-name migrations and avoids stale native self-update behavior while still keeping the interactive prompt/restart flow.
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 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:
14
+ If a newer version is available, pi-updater shows a startup prompt:
17
15
  - **Update now** — install with npm, then auto-restart pi on the current session
18
16
  - **Skip** — dismiss until next session
19
17
  - **Skip this version** — don't ask again until a newer version appears
20
18
 
21
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. Ephemeral `--no-session` runs stay ephemeral on restart.
22
20
 
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.
21
+ **`/update`:** manually check for updates (always fetches fresh from pi's update service, unless `PI_OFFLINE` is set). It installs the exact npm package/version advertised by pi's update service and respects npm engine requirements, so upgrade Node.js first if the new pi release requires it.
24
22
 
25
23
  ## How version checks work
26
24
 
package/index.ts CHANGED
@@ -1,39 +1,118 @@
1
1
  import type {
2
2
  ExtensionAPI,
3
3
  ExtensionContext,
4
- } from "@mariozechner/pi-coding-agent";
5
- import { VERSION, BorderedLoader, getAgentDir } from "@mariozechner/pi-coding-agent";
4
+ } from "@earendil-works/pi-coding-agent";
6
5
  import { spawnSync } from "node:child_process";
7
- import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
6
+ import { readFileSync, realpathSync, writeFileSync, mkdirSync } from "node:fs";
8
7
  import { join, dirname } from "node:path";
9
8
 
10
- const PACKAGE_NAME = "@mariozechner/pi-coding-agent";
9
+ const PACKAGE_NAME = "@earendil-works/pi-coding-agent";
10
+ const LEGACY_PACKAGE_NAME = "@mariozechner/pi-coding-agent";
11
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");
12
+ const NATIVE_VERSION_NOTICE_MIN_VERSION = "0.70.3";
14
13
 
15
14
  const ENV_SKIP_VERSION_CHECK = "PI_SKIP_VERSION_CHECK";
16
15
  const ENV_OFFLINE = "PI_OFFLINE";
17
16
  const ENV_INTERNAL_SKIP = "PI_UPDATER_SUPPRESSED_NATIVE_VERSION_CHECK";
18
17
 
18
+ interface LatestRelease {
19
+ version: string;
20
+ packageName?: string;
21
+ }
22
+
19
23
  interface VersionCache {
20
24
  latestVersion: string;
25
+ latestPackageName?: string;
21
26
  dismissedVersion?: string;
27
+ dismissedPackageName?: string;
22
28
  checkedAt?: string;
23
29
  }
24
30
 
25
- function readCache(): VersionCache | undefined {
31
+ type BorderedLoaderConstructor = new (...args: any[]) => any;
32
+
33
+ interface PiRuntime {
34
+ VERSION: string;
35
+ BorderedLoader: BorderedLoaderConstructor;
36
+ getAgentDir: () => string;
37
+ packageName: string;
38
+ }
39
+
40
+ let VERSION = "0.0.0";
41
+ let BorderedLoader: BorderedLoaderConstructor;
42
+ let getAgentDir: () => string;
43
+
44
+ function packageNameFromNodeModulesPath(path: string): string | undefined {
45
+ const normalized = path.replace(/\\/g, "/");
46
+ const marker = "/node_modules/";
47
+ const index = normalized.lastIndexOf(marker);
48
+ if (index === -1) return undefined;
49
+
50
+ const parts = normalized.slice(index + marker.length).split("/");
51
+ if (!parts[0]) return undefined;
52
+ if (parts[0].startsWith("@")) {
53
+ if (!parts[1]) return undefined;
54
+ return `${parts[0]}/${parts[1]}`;
55
+ }
56
+ return parts[0];
57
+ }
58
+
59
+ async function findOwningPiPackageName(pi: ExtensionAPI): Promise<string | undefined> {
60
+ try {
61
+ const cmd = process.platform === "win32" ? "where" : "which";
62
+ const result = await pi.exec(cmd, ["pi"]);
63
+ const binary = result.code === 0 ? result.stdout?.trim().split(/\r?\n/)[0] : undefined;
64
+ if (!binary) return undefined;
65
+
66
+ try {
67
+ return packageNameFromNodeModulesPath(realpathSync(binary));
68
+ } catch {
69
+ return packageNameFromNodeModulesPath(binary);
70
+ }
71
+ } catch {
72
+ return undefined;
73
+ }
74
+ }
75
+
76
+ async function loadPiRuntime(preferredPackageName?: string): Promise<PiRuntime> {
77
+ const packageNames = [
78
+ preferredPackageName,
79
+ PACKAGE_NAME,
80
+ LEGACY_PACKAGE_NAME,
81
+ ].filter((packageName): packageName is string => !!packageName);
82
+
83
+ for (const packageName of new Set(packageNames)) {
84
+ try {
85
+ const runtime = await import(packageName);
86
+ if (
87
+ typeof runtime.VERSION === "string" &&
88
+ typeof runtime.BorderedLoader === "function" &&
89
+ typeof runtime.getAgentDir === "function"
90
+ ) {
91
+ return {
92
+ VERSION: runtime.VERSION,
93
+ BorderedLoader: runtime.BorderedLoader,
94
+ getAgentDir: runtime.getAgentDir,
95
+ packageName,
96
+ };
97
+ }
98
+ } catch {}
99
+ }
100
+
101
+ throw new Error(`Could not load ${PACKAGE_NAME} or ${LEGACY_PACKAGE_NAME}`);
102
+ }
103
+
104
+ function readCache(cacheFile: string): VersionCache | undefined {
26
105
  try {
27
- return JSON.parse(readFileSync(CACHE_FILE, "utf-8"));
106
+ return JSON.parse(readFileSync(cacheFile, "utf-8"));
28
107
  } catch {
29
108
  return undefined;
30
109
  }
31
110
  }
32
111
 
33
- function writeCache(cache: VersionCache) {
112
+ function writeCache(cacheFile: string, cache: VersionCache) {
34
113
  try {
35
- mkdirSync(dirname(CACHE_FILE), { recursive: true });
36
- writeFileSync(CACHE_FILE, JSON.stringify(cache) + "\n");
114
+ mkdirSync(dirname(cacheFile), { recursive: true });
115
+ writeFileSync(cacheFile, JSON.stringify(cache) + "\n");
37
116
  } catch {}
38
117
  }
39
118
 
@@ -70,11 +149,6 @@ function compareVersions(leftVersion: string, rightVersion: string): number | un
70
149
  return left.prerelease.localeCompare(right.prerelease);
71
150
  }
72
151
 
73
- function isNewer(latest: string, current: string): boolean {
74
- const comparison = compareVersions(latest, current);
75
- return comparison !== undefined && comparison > 0;
76
- }
77
-
78
152
  function isAtLeast(version: string, minimum: string): boolean {
79
153
  const comparison = compareVersions(version, minimum);
80
154
  return comparison !== undefined && comparison >= 0;
@@ -102,20 +176,46 @@ function piUserAgent(): string {
102
176
  return `pi/${VERSION} (${process.platform}; ${runtime}; ${process.arch})`;
103
177
  }
104
178
 
105
- function hasNativeSelfUpdate(): boolean {
106
- return isAtLeast(VERSION, NATIVE_SELF_UPDATE_MIN_VERSION);
179
+ function hasNativeVersionNotice(): boolean {
180
+ return isAtLeast(VERSION, NATIVE_VERSION_NOTICE_MIN_VERSION);
181
+ }
182
+
183
+ function targetPackageName(release: LatestRelease, currentPackageName: string): string {
184
+ return release.packageName ?? currentPackageName;
185
+ }
186
+
187
+ function releaseKey(release: LatestRelease, currentPackageName: string): string {
188
+ return `${targetPackageName(release, currentPackageName)}@${release.version}`;
107
189
  }
108
190
 
109
- function saveLatestToCache(latest: string) {
110
- const prev = readCache();
111
- writeCache({
112
- latestVersion: latest,
191
+ function isUpdateAvailable(release: LatestRelease, currentPackageName: string): boolean {
192
+ const comparison = compareVersions(release.version, VERSION);
193
+ if (comparison === undefined) return false;
194
+ return comparison > 0 || (comparison === 0 && targetPackageName(release, currentPackageName) !== currentPackageName);
195
+ }
196
+
197
+ function isDismissed(
198
+ cache: VersionCache,
199
+ release: LatestRelease,
200
+ currentPackageName: string,
201
+ ): boolean {
202
+ if (cache.dismissedVersion !== release.version) return false;
203
+ if (!cache.dismissedPackageName) return !release.packageName;
204
+ return cache.dismissedPackageName === targetPackageName(release, currentPackageName);
205
+ }
206
+
207
+ function saveLatestToCache(cacheFile: string, latest: LatestRelease) {
208
+ const prev = readCache(cacheFile);
209
+ writeCache(cacheFile, {
210
+ latestVersion: latest.version,
211
+ latestPackageName: latest.packageName,
113
212
  dismissedVersion: prev?.dismissedVersion,
213
+ dismissedPackageName: prev?.dismissedPackageName,
114
214
  checkedAt: new Date().toISOString(),
115
215
  });
116
216
  }
117
217
 
118
- async function fetchLatestVersion(): Promise<string | undefined> {
218
+ async function fetchLatestRelease(): Promise<LatestRelease | undefined> {
119
219
  try {
120
220
  const res = await fetch(LATEST_VERSION_URL, {
121
221
  headers: {
@@ -125,69 +225,158 @@ async function fetchLatestVersion(): Promise<string | undefined> {
125
225
  signal: AbortSignal.timeout(10_000),
126
226
  });
127
227
  if (!res.ok) return undefined;
128
- const version = ((await res.json()) as { version?: string }).version;
129
- return typeof version === "string" && version.trim()
130
- ? version.trim()
131
- : undefined;
228
+ const data = (await res.json()) as { version?: string; packageName?: string };
229
+ if (typeof data.version !== "string" || !data.version.trim()) return undefined;
230
+ const packageName =
231
+ typeof data.packageName === "string" && data.packageName.trim()
232
+ ? data.packageName.trim()
233
+ : undefined;
234
+ return { version: data.version.trim(), packageName };
132
235
  } catch {
133
236
  return undefined;
134
237
  }
135
238
  }
136
239
 
137
240
  /** Returns a cached upgrade if available and not dismissed. */
138
- function getCachedUpgradeVersion(): string | undefined {
139
- const cache = readCache();
241
+ function getCachedUpgradeRelease(
242
+ cacheFile: string,
243
+ currentPackageName: string,
244
+ ): LatestRelease | undefined {
245
+ const cache = readCache(cacheFile);
140
246
  if (!cache) return undefined;
141
- if (!isNewer(cache.latestVersion, VERSION)) return undefined;
142
- if (cache.dismissedVersion === cache.latestVersion) return undefined;
143
- return cache.latestVersion;
247
+ const release = {
248
+ version: cache.latestVersion,
249
+ packageName: cache.latestPackageName,
250
+ };
251
+ if (!isUpdateAvailable(release, currentPackageName)) return undefined;
252
+ if (isDismissed(cache, release, currentPackageName)) return undefined;
253
+ return release;
144
254
  }
145
255
 
146
256
  /** Fetch latest from Pi's update endpoint and refresh cache. */
147
- async function refreshLatestVersionInCache(): Promise<string | undefined> {
148
- const latest = await fetchLatestVersion();
257
+ async function refreshLatestReleaseInCache(cacheFile: string): Promise<LatestRelease | undefined> {
258
+ const latest = await fetchLatestRelease();
149
259
  if (!latest) return undefined;
150
- saveLatestToCache(latest);
260
+ saveLatestToCache(cacheFile, latest);
151
261
  return latest;
152
262
  }
153
263
 
154
- function dismissVersion(version: string) {
155
- const cache = readCache();
156
- writeCache({
157
- latestVersion: cache?.latestVersion ?? version,
158
- dismissedVersion: version,
264
+ function dismissRelease(
265
+ cacheFile: string,
266
+ release: LatestRelease,
267
+ currentPackageName: string,
268
+ ) {
269
+ const cache = readCache(cacheFile);
270
+ writeCache(cacheFile, {
271
+ latestVersion: cache?.latestVersion ?? release.version,
272
+ latestPackageName: cache?.latestPackageName ?? release.packageName,
273
+ dismissedVersion: release.version,
274
+ dismissedPackageName: targetPackageName(release, currentPackageName),
159
275
  checkedAt: cache?.checkedAt,
160
276
  });
161
277
  }
162
278
 
163
- interface InstallCommand {
279
+ interface InstallStep {
164
280
  program: string;
165
281
  args: string[];
166
282
  display: string;
167
283
  }
168
284
 
169
- function getInstallCommand(version: string): InstallCommand {
170
- if (hasNativeSelfUpdate()) {
285
+ interface InstallCommand {
286
+ steps: InstallStep[];
287
+ display: string;
288
+ targetVersion: string;
289
+ targetPackageName: string;
290
+ }
291
+
292
+ interface InstallFailure {
293
+ step: InstallStep;
294
+ code: number;
295
+ output: string;
296
+ }
297
+
298
+ function npmInstallStep(packageSpec: string, args: string[] = []): InstallStep {
299
+ const stepArgs = ["install", "-g", packageSpec, ...args];
300
+ return {
301
+ program: "npm",
302
+ args: stepArgs,
303
+ display: ["npm", ...stepArgs].join(" "),
304
+ };
305
+ }
306
+
307
+ function getInstallCommand(
308
+ release: LatestRelease,
309
+ currentPackageName: string,
310
+ ): InstallCommand {
311
+ const updatePackageName = targetPackageName(release, currentPackageName);
312
+ const targetVersion = release.version;
313
+ const packageSpec = `${updatePackageName}@${targetVersion}`;
314
+ const packageChanged = updatePackageName !== currentPackageName;
315
+ const installStep = npmInstallStep(packageSpec, ["--engine-strict=true"]);
316
+
317
+ if (!packageChanged) {
171
318
  return {
172
- program: "pi",
173
- args: ["update", "--self"],
174
- display: "pi update --self",
319
+ steps: [installStep],
320
+ display: installStep.display,
321
+ targetVersion,
322
+ targetPackageName: updatePackageName,
175
323
  };
176
324
  }
177
325
 
178
326
  return {
179
- program: "npm",
180
- args: ["install", "-g", `${PACKAGE_NAME}@${version}`],
181
- display: `npm install -g ${PACKAGE_NAME}@${version}`,
327
+ steps: [
328
+ npmInstallStep(packageSpec, ["--dry-run", "--engine-strict=true"]),
329
+ npmInstallStep(packageSpec, ["--force"]),
330
+ ],
331
+ display: `migrate ${currentPackageName} → ${packageSpec}`,
332
+ targetVersion,
333
+ targetPackageName: updatePackageName,
182
334
  };
183
335
  }
184
336
 
185
- function fmtCmd(cmd: InstallCommand): string {
186
- return cmd.display;
337
+ function extractRequiredNodeVersion(output: string): string | undefined {
338
+ return (
339
+ output.match(/required:\s*\{\s*node:\s*['"]([^'"]+)['"]/i)?.[1] ??
340
+ output.match(/Required:\s*\{[^}]*"node":"([^"]+)"/i)?.[1]
341
+ );
342
+ }
343
+
344
+ function formatInstallFailure(failure: InstallFailure, cmd: InstallCommand): string {
345
+ if (/EBADENGINE|Unsupported engine|not compatible with your version of node/i.test(failure.output)) {
346
+ const requiredNode = extractRequiredNodeVersion(failure.output);
347
+ const requirement = requiredNode ? ` Requires Node.js ${requiredNode}.` : "";
348
+ return `Update blocked: pi ${cmd.targetVersion} is incompatible with current Node.js ${process.version}.${requirement} Upgrade Node.js, restart pi, then run /update again.`;
349
+ }
350
+
351
+ return `Update failed while running \`${failure.step.display}\` (exit ${failure.code})${failure.output ? `: ${failure.output}` : ""}`;
352
+ }
353
+
354
+ async function runInstallCommand(
355
+ pi: ExtensionAPI,
356
+ cmd: InstallCommand,
357
+ ): Promise<InstallFailure | undefined> {
358
+ for (const step of cmd.steps) {
359
+ const result = await pi.exec(step.program, step.args, { timeout: 120_000 });
360
+ if (result.code !== 0) {
361
+ return {
362
+ step,
363
+ code: result.code,
364
+ output: [result.stderr, result.stdout].filter(Boolean).join("\n").trim(),
365
+ };
366
+ }
367
+ }
187
368
  }
188
369
 
189
- export default function (pi: ExtensionAPI) {
190
- const suppressNativeCheck = hasNativeSelfUpdate() && !userSkippedVersionCheck;
370
+ export default async function (pi: ExtensionAPI) {
371
+ const owningPackageName = await findOwningPiPackageName(pi);
372
+ const runtime = await loadPiRuntime(owningPackageName);
373
+ VERSION = runtime.VERSION;
374
+ BorderedLoader = runtime.BorderedLoader;
375
+ getAgentDir = runtime.getAgentDir;
376
+ const currentPackageName = owningPackageName ?? runtime.packageName;
377
+
378
+ const cacheFile = join(getAgentDir(), "update-cache.json");
379
+ const suppressNativeCheck = hasNativeVersionNotice() && !userSkippedVersionCheck;
191
380
  if (suppressNativeCheck) {
192
381
  process.env[ENV_SKIP_VERSION_CHECK] = "1";
193
382
  process.env[ENV_INTERNAL_SKIP] = "1";
@@ -245,31 +434,10 @@ export default function (pi: ExtensionAPI) {
245
434
  const loader = new BorderedLoader(tui, theme, `Running ${cmd.display}...`);
246
435
  loader.onAbort = () => done(false);
247
436
 
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()
263
- .then((result) => {
264
- if (result.code !== 0) {
265
- const output = [result.stderr, result.stdout]
266
- .filter(Boolean)
267
- .join("\n")
268
- .trim();
269
- ctx.ui.notify(
270
- `Update failed (exit ${result.code})${output ? `: ${output}` : ""}`,
271
- "error",
272
- );
437
+ runInstallCommand(pi, cmd)
438
+ .then((failure) => {
439
+ if (failure) {
440
+ ctx.ui.notify(formatInstallFailure(failure, cmd), "error");
273
441
  done(false);
274
442
  } else {
275
443
  done(true);
@@ -319,36 +487,39 @@ export default function (pi: ExtensionAPI) {
319
487
  );
320
488
  }
321
489
 
322
- async function showUpdatePrompt(ctx: ExtensionContext, latest: string) {
323
- const cmd = getInstallCommand(latest);
324
- const choice = await ctx.ui.select(`Update ${VERSION} → ${latest}`, [
325
- `Update now (${fmtCmd(cmd)})`,
490
+ async function showUpdatePrompt(ctx: ExtensionContext, latest: LatestRelease) {
491
+ const cmd = getInstallCommand(latest, currentPackageName);
492
+ const currentLabel = `${currentPackageName}@${VERSION}`;
493
+ const targetLabel = `${cmd.targetPackageName}@${cmd.targetVersion}`;
494
+ const choice = await ctx.ui.select(`Update ${currentLabel} → ${targetLabel}`, [
495
+ `Update now (${cmd.display})`,
326
496
  "Skip",
327
497
  "Skip this version",
328
498
  ]);
329
499
 
330
500
  if (!choice || choice === "Skip") return;
331
501
  if (choice === "Skip this version") {
332
- dismissVersion(latest);
502
+ dismissRelease(cacheFile, latest, currentPackageName);
333
503
  return;
334
504
  }
335
- await doInstall(ctx, latest, cmd);
505
+ await doInstall(ctx, targetLabel, cmd);
336
506
  }
337
507
 
338
- function canAutoPromptVersion(latest: string): boolean {
339
- if (!isNewer(latest, VERSION)) return false;
340
- if (promptedVersions.has(latest)) return false;
341
- if (readCache()?.dismissedVersion === latest) return false;
508
+ function canAutoPromptVersion(latest: LatestRelease): boolean {
509
+ if (!isUpdateAvailable(latest, currentPackageName)) return false;
510
+ if (promptedVersions.has(releaseKey(latest, currentPackageName))) return false;
511
+ const cache = readCache(cacheFile);
512
+ if (cache && isDismissed(cache, latest, currentPackageName)) return false;
342
513
  return true;
343
514
  }
344
515
 
345
- async function maybeShowAutoPrompt(ctx: ExtensionContext, latest: string) {
516
+ async function maybeShowAutoPrompt(ctx: ExtensionContext, latest: LatestRelease) {
346
517
  if (!ctx.hasUI) return;
347
518
  if (promptOpen) return;
348
519
  if (!canAutoPromptVersion(latest)) return;
349
520
 
350
521
  promptOpen = true;
351
- promptedVersions.add(latest);
522
+ promptedVersions.add(releaseKey(latest, currentPackageName));
352
523
  try {
353
524
  await showUpdatePrompt(ctx, latest);
354
525
  } finally {
@@ -360,13 +531,13 @@ export default function (pi: ExtensionAPI) {
360
531
  if (!ctx.hasUI) return;
361
532
  if (shouldSkipAutoChecks()) return;
362
533
 
363
- const cached = getCachedUpgradeVersion();
534
+ const cached = getCachedUpgradeRelease(cacheFile, currentPackageName);
364
535
  if (cached) void maybeShowAutoPrompt(ctx, cached);
365
536
 
366
537
  if (liveCheckStarted) return;
367
538
  liveCheckStarted = true;
368
539
 
369
- void refreshLatestVersionInCache()
540
+ void refreshLatestReleaseInCache(cacheFile)
370
541
  .then((latest) => {
371
542
  if (!latest) return;
372
543
  void maybeShowAutoPrompt(ctx, latest);
@@ -380,14 +551,14 @@ export default function (pi: ExtensionAPI) {
380
551
  });
381
552
 
382
553
  pi.registerCommand("update", {
383
- description: "Check for pi updates and install via native updater when available",
554
+ description: "Check for pi updates and install with npm",
384
555
  handler: async (rawArgs, ctx) => {
385
556
  // /update --test — simulate the full UI flow without a real install
386
557
  if (rawArgs?.trim() === "--test") {
387
558
  const fakeLatest = "99.0.0";
388
- const cmd = getInstallCommand(fakeLatest);
389
- const choice = await ctx.ui.select(`Update ${VERSION} → ${fakeLatest}`, [
390
- `Update now (${fmtCmd(cmd)})`,
559
+ const cmd = getInstallCommand({ version: fakeLatest }, currentPackageName);
560
+ const choice = await ctx.ui.select(`Update ${currentPackageName}@${VERSION} → ${cmd.targetPackageName}@${fakeLatest}`, [
561
+ `Update now (${cmd.display})`,
391
562
  "Skip",
392
563
  "Skip this version",
393
564
  ]);
@@ -422,7 +593,7 @@ export default function (pi: ExtensionAPI) {
422
593
  return;
423
594
  }
424
595
 
425
- const latest = await ctx.ui.custom<string | null>(
596
+ const latest = await ctx.ui.custom<LatestRelease | null>(
426
597
  (tui, theme, _kb, done) => {
427
598
  const loader = new BorderedLoader(
428
599
  tui,
@@ -430,7 +601,7 @@ export default function (pi: ExtensionAPI) {
430
601
  "Checking for updates...",
431
602
  );
432
603
  loader.onAbort = () => done(null);
433
- fetchLatestVersion()
604
+ fetchLatestRelease()
434
605
  .then((v) => done(v ?? null))
435
606
  .catch(() => done(null));
436
607
  return loader;
@@ -442,14 +613,14 @@ export default function (pi: ExtensionAPI) {
442
613
  return;
443
614
  }
444
615
 
445
- saveLatestToCache(latest);
616
+ saveLatestToCache(cacheFile, latest);
446
617
 
447
- if (!isNewer(latest, VERSION)) {
448
- ctx.ui.notify(`Already on latest version (${VERSION}).`, "info");
618
+ if (!isUpdateAvailable(latest, currentPackageName)) {
619
+ ctx.ui.notify(`Already on latest version (${currentPackageName}@${VERSION}).`, "info");
449
620
  return;
450
621
  }
451
622
 
452
- promptedVersions.add(latest);
623
+ promptedVersions.add(releaseKey(latest, currentPackageName));
453
624
  await showUpdatePrompt(ctx, latest);
454
625
  },
455
626
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-updater",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
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",
@@ -26,10 +26,15 @@
26
26
  "CHANGELOG.md"
27
27
  ],
28
28
  "peerDependencies": {
29
- "@mariozechner/pi-coding-agent": "*"
29
+ "@earendil-works/pi-coding-agent": "*"
30
+ },
31
+ "peerDependenciesMeta": {
32
+ "@earendil-works/pi-coding-agent": {
33
+ "optional": true
34
+ }
30
35
  },
31
36
  "devDependencies": {
32
- "@mariozechner/pi-coding-agent": "^0.65.0",
37
+ "@earendil-works/pi-coding-agent": "^0.74.1",
33
38
  "@types/node": "^25.3.2",
34
39
  "typescript": "^5.9.3"
35
40
  }