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.
- package/CHANGELOG.md +12 -0
- package/README.md +3 -5
- package/index.ts +273 -102
- 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:**
|
|
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
|
-
|
|
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).
|
|
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 "@
|
|
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 = "@
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
36
|
-
writeFileSync(
|
|
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
|
|
106
|
-
return isAtLeast(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
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
139
|
-
|
|
241
|
+
function getCachedUpgradeRelease(
|
|
242
|
+
cacheFile: string,
|
|
243
|
+
currentPackageName: string,
|
|
244
|
+
): LatestRelease | undefined {
|
|
245
|
+
const cache = readCache(cacheFile);
|
|
140
246
|
if (!cache) return undefined;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
148
|
-
const latest = await
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
279
|
+
interface InstallStep {
|
|
164
280
|
program: string;
|
|
165
281
|
args: string[];
|
|
166
282
|
display: string;
|
|
167
283
|
}
|
|
168
284
|
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
319
|
+
steps: [installStep],
|
|
320
|
+
display: installStep.display,
|
|
321
|
+
targetVersion,
|
|
322
|
+
targetPackageName: updatePackageName,
|
|
175
323
|
};
|
|
176
324
|
}
|
|
177
325
|
|
|
178
326
|
return {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
186
|
-
return
|
|
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
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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:
|
|
323
|
-
const cmd = getInstallCommand(latest);
|
|
324
|
-
const
|
|
325
|
-
|
|
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
|
-
|
|
502
|
+
dismissRelease(cacheFile, latest, currentPackageName);
|
|
333
503
|
return;
|
|
334
504
|
}
|
|
335
|
-
await doInstall(ctx,
|
|
505
|
+
await doInstall(ctx, targetLabel, cmd);
|
|
336
506
|
}
|
|
337
507
|
|
|
338
|
-
function canAutoPromptVersion(latest:
|
|
339
|
-
if (!
|
|
340
|
-
if (promptedVersions.has(latest)) return false;
|
|
341
|
-
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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 (${
|
|
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<
|
|
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
|
-
|
|
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 (!
|
|
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.
|
|
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
|
-
"@
|
|
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
|
-
"@
|
|
37
|
+
"@earendil-works/pi-coding-agent": "^0.74.1",
|
|
33
38
|
"@types/node": "^25.3.2",
|
|
34
39
|
"typescript": "^5.9.3"
|
|
35
40
|
}
|