pi-updater 0.2.9 → 0.3.1

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.1 - 2026-04-04
4
+
5
+ - 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
+ - Store cache and dismissed-version state in pi's configured agent directory.
7
+ - Preserve `--no-session` mode when restarting after an update and show the correct manual restart hint.
8
+
9
+ ## Unreleased
10
+
11
+ ## 0.3.0 - 2026-03-23
12
+
13
+ - Auto-restart pi after a successful update. Asks to restart, then seamlessly relaunches on the current session.
14
+ - Falls back to manual restart message in non-interactive modes or if restart fails.
15
+ - Cross-platform: uses `shell: true` on Windows to handle `.cmd` shims.
16
+ - `/update --test` to simulate the full update flow without a real install.
17
+
3
18
  ## 0.2.9 - 2026-03-16
4
19
 
5
20
  - Keep startup checks cache-first and non-blocking.
package/README.md CHANGED
@@ -12,10 +12,12 @@ A lightweight, Codex-style auto-updater for pi with fast, cache-first startup ch
12
12
  ## What it does
13
13
 
14
14
  **On startup:** if a newer version is available, shows a prompt:
15
- - **Update now** — install with npm, then restart pi
15
+ - **Update now** — install with npm, then auto-restart pi on the current session
16
16
  - **Skip** — dismiss until next session
17
17
  - **Skip this version** — don't ask again until a newer version appears
18
18
 
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.
20
+
19
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.
20
22
 
21
23
  **`/update`:** manually check for updates (always fetches fresh from npm, unless `PI_OFFLINE` is set).
@@ -45,6 +47,8 @@ pi install git:github.com/tonze/pi-updater
45
47
 
46
48
  Use `/update` inside pi to manually check for updates and install them.
47
49
 
50
+ Cache and dismissed-version state are stored in pi's configured agent directory and respect `PI_CODING_AGENT_DIR`.
51
+
48
52
  ## Environment flags
49
53
 
50
54
  Disable automatic version checks:
package/index.ts CHANGED
@@ -2,14 +2,14 @@ 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
+ import { spawnSync } from "node:child_process";
6
7
  import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
7
8
  import { join, dirname } from "node:path";
8
- import { homedir } from "node:os";
9
9
 
10
10
  const PACKAGE_NAME = "@mariozechner/pi-coding-agent";
11
11
  const REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
12
- const CACHE_FILE = join(homedir(), ".pi", "agent", "update-cache.json");
12
+ const CACHE_FILE = join(getAgentDir(), "update-cache.json");
13
13
 
14
14
  const ENV_SKIP_VERSION_CHECK = "PI_SKIP_VERSION_CHECK";
15
15
  const ENV_OFFLINE = "PI_OFFLINE";
@@ -127,6 +127,40 @@ export default function (pi: ExtensionAPI) {
127
127
  const promptedVersions = new Set<string>();
128
128
  let liveCheckStarted = false;
129
129
 
130
+ async function findPiBinary(): Promise<string> {
131
+ const cmd = process.platform === "win32" ? "where" : "which";
132
+ const result = await pi.exec(cmd, ["pi"]);
133
+ if (result.code === 0 && result.stdout?.trim()) {
134
+ return result.stdout.trim().split(/\r?\n/)[0];
135
+ }
136
+ return "pi";
137
+ }
138
+
139
+ function canAutoRestart(ctx: ExtensionContext): boolean {
140
+ return ctx.hasUI && !!process.stdin.isTTY && !!process.stdout.isTTY;
141
+ }
142
+
143
+ async function restartPi(ctx: ExtensionContext): Promise<boolean> {
144
+ const piBinary = await findPiBinary();
145
+ const sessionFile = ctx.sessionManager.getSessionFile();
146
+ const restartArgs = sessionFile ? ["--session", sessionFile] : ["--no-session"];
147
+
148
+ return ctx.ui.custom<boolean>((tui, _theme, _kb, done) => {
149
+ tui.stop();
150
+ const result = spawnSync(piBinary, restartArgs, {
151
+ cwd: ctx.cwd,
152
+ env: process.env,
153
+ stdio: "inherit",
154
+ shell: process.platform === "win32",
155
+ windowsHide: false,
156
+ });
157
+ tui.start();
158
+ tui.requestRender(true);
159
+ done(!result.error && (result.status === null || result.status === 0));
160
+ return { render: () => [], invalidate: () => {} };
161
+ });
162
+ }
163
+
130
164
  async function doInstall(
131
165
  ctx: ExtensionContext,
132
166
  latest: string,
@@ -136,37 +170,54 @@ export default function (pi: ExtensionAPI) {
136
170
  const loader = new BorderedLoader(tui, theme, `Installing ${latest}...`);
137
171
  loader.onAbort = () => done(false);
138
172
 
139
- const run = async () => {
140
- if (cmd.program === "echo") {
141
- ctx.ui.notify(cmd.args[0], "info");
142
- return false;
143
- }
144
- const result = await pi.exec(cmd.program, cmd.args, {
145
- timeout: 120_000,
146
- });
147
- if (result.code !== 0) {
148
- ctx.ui.notify(
149
- `Update failed (exit ${result.code}): ${result.stderr || result.stdout}`,
150
- "error",
151
- );
152
- return false;
153
- }
154
- return true;
155
- };
156
-
157
- run()
158
- .then(done)
173
+ pi.exec(cmd.program, cmd.args, { timeout: 120_000 })
174
+ .then((result) => {
175
+ if (result.code !== 0) {
176
+ ctx.ui.notify(
177
+ `Update failed (exit ${result.code}): ${result.stderr || result.stdout}`,
178
+ "error",
179
+ );
180
+ done(false);
181
+ } else {
182
+ done(true);
183
+ }
184
+ })
159
185
  .catch(() => done(false));
186
+
160
187
  return loader;
161
188
  });
162
189
 
163
190
  if (!success) return;
164
191
 
165
- const ok = await ctx.ui.confirm(
192
+ const restartTip = ctx.sessionManager.getSessionFile()
193
+ ? "Tip: run `pi -c` to continue this session."
194
+ : "Tip: run `pi --no-session` to continue without a saved session.";
195
+
196
+ if (!canAutoRestart(ctx)) {
197
+ ctx.ui.notify(
198
+ `Updated to ${latest}! Please restart pi.\n${restartTip}`,
199
+ "info",
200
+ );
201
+ return;
202
+ }
203
+
204
+ const restart = await ctx.ui.confirm(
166
205
  `Updated to ${latest}!`,
167
- "Shut down pi? (Use pi -c to continue this session)",
206
+ "Restart now?",
207
+ );
208
+
209
+ if (!restart) return;
210
+
211
+ const ok = await restartPi(ctx);
212
+ if (ok) {
213
+ ctx.shutdown();
214
+ return;
215
+ }
216
+
217
+ ctx.ui.notify(
218
+ `Updated to ${latest}! Auto-restart failed. Please restart pi manually.\n${restartTip}`,
219
+ "error",
168
220
  );
169
- if (ok) ctx.shutdown();
170
221
  }
171
222
 
172
223
  async function showUpdatePrompt(ctx: ExtensionContext, latest: string) {
@@ -224,17 +275,46 @@ export default function (pi: ExtensionAPI) {
224
275
  .catch(() => {});
225
276
  }
226
277
 
227
- pi.on("session_start", async (_event, ctx) => {
228
- runAutoChecks(ctx);
229
- });
230
-
231
- pi.on("session_switch", async (_event, ctx) => {
278
+ pi.on("session_start", async (event, ctx) => {
279
+ if (event.reason === "reload" || event.reason === "fork") return;
232
280
  runAutoChecks(ctx);
233
281
  });
234
282
 
235
283
  pi.registerCommand("update", {
236
284
  description: "Check for pi updates and install",
237
- handler: async (_args, ctx) => {
285
+ handler: async (rawArgs, ctx) => {
286
+ // /update --test — simulate the full UI flow without a real install
287
+ if (rawArgs?.trim() === "--test") {
288
+ const fakeLatest = "99.0.0";
289
+ const cmd = getInstallCommand(fakeLatest);
290
+ const choice = await ctx.ui.select(`Update ${VERSION} → ${fakeLatest}`, [
291
+ `Update now (${fmtCmd(cmd)})`,
292
+ "Skip",
293
+ "Skip this version",
294
+ ]);
295
+ if (!choice || choice === "Skip" || choice === "Skip this version") return;
296
+
297
+ await ctx.ui.custom<void>((tui, theme, _kb, done) => {
298
+ const loader = new BorderedLoader(tui, theme, `Installing ${fakeLatest}...`);
299
+ loader.onAbort = () => done();
300
+ setTimeout(() => done(), 1500);
301
+ return loader;
302
+ });
303
+
304
+ if (!canAutoRestart(ctx)) {
305
+ ctx.ui.notify(`Updated to ${fakeLatest}! Please restart pi.`, "info");
306
+ return;
307
+ }
308
+
309
+ const restart = await ctx.ui.confirm(`Updated to ${fakeLatest}!`, "Restart now?");
310
+ if (!restart) return;
311
+
312
+ const ok = await restartPi(ctx);
313
+ if (ok) { ctx.shutdown(); return; }
314
+ ctx.ui.notify("Test restart failed.", "error");
315
+ return;
316
+ }
317
+
238
318
  if (isOffline()) {
239
319
  ctx.ui.notify(
240
320
  "PI_OFFLINE is set. Disable it to check for updates.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-updater",
3
- "version": "0.2.9",
3
+ "version": "0.3.1",
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
  }