pi-updater 0.2.9 → 0.3.0

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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 - 2026-03-23
4
+
5
+ - Auto-restart pi after a successful update. Asks to restart, then seamlessly relaunches on the current session.
6
+ - Falls back to manual restart message in non-interactive modes or if restart fails.
7
+ - Cross-platform: uses `shell: true` on Windows to handle `.cmd` shims.
8
+ - `/update --test` to simulate the full update flow without a real install.
9
+
3
10
  ## 0.2.9 - 2026-03-16
4
11
 
5
12
  - 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.
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).
package/index.ts CHANGED
@@ -3,6 +3,7 @@ import type {
3
3
  ExtensionContext,
4
4
  } from "@mariozechner/pi-coding-agent";
5
5
  import { VERSION, BorderedLoader } 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
9
  import { homedir } from "node:os";
@@ -127,6 +128,40 @@ export default function (pi: ExtensionAPI) {
127
128
  const promptedVersions = new Set<string>();
128
129
  let liveCheckStarted = false;
129
130
 
131
+ async function findPiBinary(): Promise<string> {
132
+ const cmd = process.platform === "win32" ? "where" : "which";
133
+ const result = await pi.exec(cmd, ["pi"]);
134
+ if (result.code === 0 && result.stdout?.trim()) {
135
+ return result.stdout.trim().split(/\r?\n/)[0];
136
+ }
137
+ return "pi";
138
+ }
139
+
140
+ function canAutoRestart(ctx: ExtensionContext): boolean {
141
+ return ctx.hasUI && !!process.stdin.isTTY && !!process.stdout.isTTY;
142
+ }
143
+
144
+ async function restartPi(ctx: ExtensionContext): Promise<boolean> {
145
+ const piBinary = await findPiBinary();
146
+ const sessionFile = ctx.sessionManager.getSessionFile();
147
+ const restartArgs = sessionFile ? ["--session", sessionFile] : ["-c"];
148
+
149
+ return ctx.ui.custom<boolean>((tui, _theme, _kb, done) => {
150
+ tui.stop();
151
+ const result = spawnSync(piBinary, restartArgs, {
152
+ cwd: ctx.cwd,
153
+ env: process.env,
154
+ stdio: "inherit",
155
+ shell: process.platform === "win32",
156
+ windowsHide: false,
157
+ });
158
+ tui.start();
159
+ tui.requestRender(true);
160
+ done(!result.error && (result.status === null || result.status === 0));
161
+ return { render: () => [], invalidate: () => {} };
162
+ });
163
+ }
164
+
130
165
  async function doInstall(
131
166
  ctx: ExtensionContext,
132
167
  latest: string,
@@ -136,37 +171,50 @@ export default function (pi: ExtensionAPI) {
136
171
  const loader = new BorderedLoader(tui, theme, `Installing ${latest}...`);
137
172
  loader.onAbort = () => done(false);
138
173
 
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)
174
+ pi.exec(cmd.program, cmd.args, { timeout: 120_000 })
175
+ .then((result) => {
176
+ if (result.code !== 0) {
177
+ ctx.ui.notify(
178
+ `Update failed (exit ${result.code}): ${result.stderr || result.stdout}`,
179
+ "error",
180
+ );
181
+ done(false);
182
+ } else {
183
+ done(true);
184
+ }
185
+ })
159
186
  .catch(() => done(false));
187
+
160
188
  return loader;
161
189
  });
162
190
 
163
191
  if (!success) return;
164
192
 
165
- const ok = await ctx.ui.confirm(
193
+ if (!canAutoRestart(ctx)) {
194
+ ctx.ui.notify(
195
+ `Updated to ${latest}! Please restart pi.\nTip: run \`pi -c\` to continue this session.`,
196
+ "info",
197
+ );
198
+ return;
199
+ }
200
+
201
+ const restart = await ctx.ui.confirm(
166
202
  `Updated to ${latest}!`,
167
- "Shut down pi? (Use pi -c to continue this session)",
203
+ "Restart now?",
204
+ );
205
+
206
+ if (!restart) return;
207
+
208
+ const ok = await restartPi(ctx);
209
+ if (ok) {
210
+ ctx.shutdown();
211
+ return;
212
+ }
213
+
214
+ ctx.ui.notify(
215
+ `Updated to ${latest}! Auto-restart failed. Please restart pi manually.\nTip: run \`pi -c\` to continue this session.`,
216
+ "error",
168
217
  );
169
- if (ok) ctx.shutdown();
170
218
  }
171
219
 
172
220
  async function showUpdatePrompt(ctx: ExtensionContext, latest: string) {
@@ -234,7 +282,39 @@ export default function (pi: ExtensionAPI) {
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.0",
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",