@pi-unipi/updater 0.1.1 → 2.0.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/README.md CHANGED
@@ -1,22 +1,44 @@
1
1
  # @pi-unipi/updater
2
2
 
3
- Auto-updater, changelog browser, and readme browser for the Unipi extension suite.
3
+ Checks npm for new versions on session start, shows a changelog diff, and lets you update with one keypress. Also provides TUI browsers for package READMEs and the changelog.
4
4
 
5
- ## Features
6
-
7
- - **Auto-updater** — Periodically checks npm registry for new versions, shows update prompt with changelog diff
8
- - **Changelog browser** — TUI overlay listing all versions with dates and status labels (✓ Current, ↑ New)
9
- - **Readme browser** — TUI overlay listing all packages with versions, opens rendered README content
5
+ The update overlay appears automatically when a newer version is found. Press `Y` to update, `n` to skip. Skipped versions are cached — you only get re-prompted when an even newer version appears.
10
6
 
11
7
  ## Commands
12
8
 
13
9
  | Command | Description |
14
10
  |---------|-------------|
15
- | `/unipi:readme [package]` | Browse package README files. No arg opens list, with arg opens directly. |
11
+ | `/unipi:readme [package]` | Browse package README files in TUI overlay |
16
12
  | `/unipi:changelog` | Browse CHANGELOG.md with version list and detail view |
17
13
  | `/unipi:updater-settings` | Configure check interval and auto-update mode |
18
14
 
19
- ## Configuration
15
+ ### TUI Controls
16
+
17
+ | Key | Action |
18
+ |-----|--------|
19
+ | `j/k` or Up/Down | Navigate |
20
+ | `Enter` | Select/open |
21
+ | `q/Esc` | Back/close |
22
+ | `g/G` | Jump to top/bottom |
23
+ | `Space` | Cycle options (settings) |
24
+ | `h/l` or Left/Right | Cycle options (settings) |
25
+
26
+ ## Special Triggers
27
+
28
+ On session start, updater checks the npm registry for `@pi-unipi/unipi`. If a newer version exists and wasn't previously skipped, it shows the update overlay with changelog diff. This runs once per session, respecting the check interval config.
29
+
30
+ Updater registers with the info-screen dashboard, showing installed version, latest version, update status, and last check time.
31
+
32
+ ## How Updates Work
33
+
34
+ 1. Session start triggers npm registry check
35
+ 2. Compare latest version with installed version
36
+ 3. If newer and not skipped, show update overlay
37
+ 4. User views changelog diff, presses `Y` to update or `n` to skip
38
+ 5. Update runs `pi install npm:@pi-unipi/unipi`
39
+ 6. Skipped version cached — re-prompted only for newer versions
40
+
41
+ ## Configurables
20
42
 
21
43
  Config stored at `~/.unipi/config/updater/config.json`:
22
44
 
@@ -27,41 +49,21 @@ Config stored at `~/.unipi/config/updater/config.json`:
27
49
  }
28
50
  ```
29
51
 
30
- ### Options
31
-
32
52
  | Option | Values | Default |
33
53
  |--------|--------|---------|
34
- | `checkIntervalMs` | `1800000` (30min), `3600000` (1h), `21600000` (6h), `86400000` (1d) | `3600000` (1h) |
35
- | `autoUpdate` | `disabled`, `notify`, `auto` | `notify` |
54
+ | `checkIntervalMs` | 1800000 (30min), 3600000 (1h), 21600000 (6h), 86400000 (1d) | 3600000 (1h) |
55
+ | `autoUpdate` | disabled, notify, auto | notify |
36
56
 
37
- ### Auto-update modes
57
+ ### Auto-update Modes
38
58
 
39
59
  - **disabled** — No update checks on session start
40
- - **notify** — Show update overlay with changelog diff, user chooses [Y] Update or [n] Skip
41
- - **auto** — Show countdown overlay, auto-install after 5 seconds unless cancelled
42
-
43
- ## How it works
44
-
45
- 1. On session start, checks npm registry for `@pi-unipi/unipi` latest version
46
- 2. Compares with installed version from `package.json`
47
- 3. If newer version found and not previously skipped, shows update overlay
48
- 4. User can view changelog diff, update with `[Y]`, or skip with `[n]`
49
- 5. Skipped versions are cached — only re-prompted when an even newer version appears
50
- 6. Update installs via `pi install npm:@pi-unipi/unipi`
60
+ - **notify** — Show overlay with changelog, user chooses Y/n
61
+ - **auto** — Show countdown, auto-install after 5 seconds unless cancelled
51
62
 
52
- ## TUI Overlays
63
+ ### Cache
53
64
 
54
- All overlays use keyboard navigation:
55
- - `j`/`k` or ↑/↓ — navigate
56
- - `Enter` — select/open
57
- - `q`/`Esc` — back/close
58
- - `g`/`G` — jump to top/bottom
59
- - `Space` — cycle options (settings overlay)
60
- - `h`/`l` or ←/→ — cycle options (settings overlay)
65
+ Last-check cache at `~/.unipi/cache/updater/last-check.json`:
61
66
 
62
- ## Cache
63
-
64
- Last-check cache stored at `~/.unipi/cache/updater/last-check.json`:
65
67
  ```json
66
68
  {
67
69
  "lastCheck": "2026-05-01T12:00:00.000Z",
@@ -69,3 +71,7 @@ Last-check cache stored at `~/.unipi/cache/updater/last-check.json`:
69
71
  "skippedVersion": "0.1.16"
70
72
  }
71
73
  ```
74
+
75
+ ## License
76
+
77
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/updater",
3
- "version": "0.1.1",
3
+ "version": "2.0.0",
4
4
  "description": "Auto-updater, changelog browser, and readme browser for Unipi — checks npm registry, renders CHANGELOG.md and README.md files in TUI overlays",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/checker.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * respects check interval from config/cache.
6
6
  */
7
7
 
8
- import { getPackageVersion } from "@pi-unipi/core";
8
+ import { getInstalledPackageVersion } from "@pi-unipi/core";
9
9
  import { loadConfig } from "./settings.js";
10
10
  import { readLastCheck, writeLastCheck, isCheckDue } from "./cache.js";
11
11
  import type { UpdateCheckResult } from "../types.js";
@@ -15,9 +15,9 @@ const NPM_REGISTRY_URL = "https://registry.npmjs.org/@pi-unipi/unipi";
15
15
 
16
16
  /** Resolve the installed version of @pi-unipi/unipi */
17
17
  function getInstalledVersion(): string {
18
- // Walk up from this file to find the monorepo root
19
- const dir = new URL("../../..", import.meta.url).pathname;
20
- return getPackageVersion(dir);
18
+ // Walk up from this file to find the @pi-unipi/unipi package by name
19
+ const dir = new URL("..", import.meta.url).pathname;
20
+ return getInstalledPackageVersion(dir, "@pi-unipi/unipi");
21
21
  }
22
22
 
23
23
  /**
@@ -71,14 +71,14 @@ export async function checkForUpdates(): Promise<UpdateCheckResult> {
71
71
  latestVersion,
72
72
  currentVersion,
73
73
  };
74
- } catch (err: any) {
74
+ } catch (err: unknown) {
75
75
  // Network error — return cached info if available
76
76
  const cache = readLastCheck();
77
77
  return {
78
78
  updateAvailable: false,
79
79
  latestVersion: cache?.latestVersion ?? "",
80
80
  currentVersion,
81
- error: err.message ?? "Unknown error",
81
+ error: err instanceof Error ? err.message : String(err) || "Unknown error",
82
82
  };
83
83
  }
84
84
  }
package/src/commands.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Registers /unipi:readme [package], /unipi:changelog, /unipi:updater-settings
5
5
  */
6
6
 
7
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
7
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
8
8
  import { UNIPI_PREFIX, UPDATER_COMMANDS } from "@pi-unipi/core";
9
9
  import { renderReadmeOverlay } from "./tui/readme-overlay.js";
10
10
  import { renderChangelogOverlay } from "./tui/changelog-overlay.js";
@@ -14,9 +14,9 @@ import { renderSettingsOverlay } from "./tui/settings-overlay.js";
14
14
  const OVERLAY_OPTIONS = {
15
15
  overlay: true,
16
16
  overlayOptions: {
17
- width: "80%",
17
+ width: "80%" as const,
18
18
  minWidth: 60,
19
- anchor: "center",
19
+ anchor: "center" as const,
20
20
  margin: 2,
21
21
  },
22
22
  };
@@ -28,7 +28,7 @@ export function registerCommands(pi: ExtensionAPI): void {
28
28
  `${UNIPI_PREFIX}${UPDATER_COMMANDS.README}`,
29
29
  {
30
30
  description: "Browse package README files",
31
- handler: async (args: string, ctx: any) => {
31
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
32
32
  const packageName = args.trim() || undefined;
33
33
  try {
34
34
  await ctx.ui.custom(
@@ -47,7 +47,7 @@ export function registerCommands(pi: ExtensionAPI): void {
47
47
  `${UNIPI_PREFIX}${UPDATER_COMMANDS.CHANGELOG}`,
48
48
  {
49
49
  description: "Browse changelog (Keep a Changelog format)",
50
- handler: async (_args: string, ctx: any) => {
50
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
51
51
  try {
52
52
  await ctx.ui.custom(
53
53
  renderChangelogOverlay(),
@@ -65,7 +65,7 @@ export function registerCommands(pi: ExtensionAPI): void {
65
65
  `${UNIPI_PREFIX}${UPDATER_COMMANDS.UPDATER_SETTINGS}`,
66
66
  {
67
67
  description: "Configure updater — check interval and auto-update mode",
68
- handler: async (_args: string, ctx: any) => {
68
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
69
69
  try {
70
70
  const result = await ctx.ui.custom(
71
71
  renderSettingsOverlay(),
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  UNIPI_PREFIX,
17
17
  emitEvent,
18
18
  getPackageVersion,
19
+ type UnipiUpdateCheckEvent,
19
20
  } from "@pi-unipi/core";
20
21
  import { registerCommands } from "./commands.js";
21
22
  import { loadConfig } from "./settings.js";
@@ -41,7 +42,7 @@ export default function updaterExtension(pi: ExtensionAPI): void {
41
42
  // Session lifecycle — check for updates and announce module
42
43
  pi.on("session_start", async (_event, ctx) => {
43
44
  // Emit MODULE_READY
44
- emitEvent(pi as any, UNIPI_EVENTS.MODULE_READY, {
45
+ emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
45
46
  name: MODULES.UPDATER,
46
47
  version: VERSION,
47
48
  commands: [
@@ -53,7 +54,7 @@ export default function updaterExtension(pi: ExtensionAPI): void {
53
54
  });
54
55
 
55
56
  // Register info-screen group
56
- const infoRegistry = (globalThis as any).__unipi_info_registry;
57
+ const infoRegistry = globalThis.__unipi_info_registry;
57
58
  if (infoRegistry) {
58
59
  let cachedResult: { currentVersion: string; latestVersion: string; updateAvailable: boolean; lastCheck: string } | null = null;
59
60
 
@@ -90,34 +91,35 @@ export default function updaterExtension(pi: ExtensionAPI): void {
90
91
  });
91
92
 
92
93
  // Subscribe to events to update cached data
93
- pi.events.on(UNIPI_EVENTS.UPDATE_CHECK, (payload: any) => {
94
+ pi.events.on(UNIPI_EVENTS.UPDATE_CHECK, (data) => {
95
+ const payload = data as UnipiUpdateCheckEvent;
94
96
  cachedResult = {
95
97
  currentVersion: payload.currentVersion,
96
98
  latestVersion: payload.latestVersion,
97
99
  updateAvailable: payload.updateAvailable,
98
100
  lastCheck: new Date().toLocaleTimeString(),
99
101
  };
100
- emitEvent(pi as any, UNIPI_EVENTS.INFO_DATA_UPDATED, {
102
+ emitEvent(pi, UNIPI_EVENTS.INFO_DATA_UPDATED, {
101
103
  groupId: "updater",
102
104
  keys: ["current", "latest", "status", "lastCheck"],
103
105
  });
104
106
  });
105
107
 
106
- pi.events.on(UNIPI_EVENTS.UPDATE_AVAILABLE, (_payload: any) => {
108
+ pi.events.on(UNIPI_EVENTS.UPDATE_AVAILABLE, (_data: unknown) => {
107
109
  if (cachedResult) {
108
110
  cachedResult.updateAvailable = true;
109
111
  }
110
- emitEvent(pi as any, UNIPI_EVENTS.INFO_DATA_UPDATED, {
112
+ emitEvent(pi, UNIPI_EVENTS.INFO_DATA_UPDATED, {
111
113
  groupId: "updater",
112
114
  keys: ["status"],
113
115
  });
114
116
  });
115
117
 
116
- pi.events.on(UNIPI_EVENTS.UPDATE_APPLIED, (_payload: any) => {
118
+ pi.events.on(UNIPI_EVENTS.UPDATE_APPLIED, (_data: unknown) => {
117
119
  if (cachedResult) {
118
120
  cachedResult.updateAvailable = false;
119
121
  }
120
- emitEvent(pi as any, UNIPI_EVENTS.INFO_DATA_UPDATED, {
122
+ emitEvent(pi, UNIPI_EVENTS.INFO_DATA_UPDATED, {
121
123
  groupId: "updater",
122
124
  keys: ["status"],
123
125
  });
@@ -132,7 +134,7 @@ export default function updaterExtension(pi: ExtensionAPI): void {
132
134
  const result = await checkForUpdates();
133
135
 
134
136
  // Emit check event
135
- emitEvent(pi as any, UNIPI_EVENTS.UPDATE_CHECK, result);
137
+ emitEvent(pi, UNIPI_EVENTS.UPDATE_CHECK, result);
136
138
 
137
139
  if (!result.updateAvailable || result.error) return;
138
140
 
@@ -140,7 +142,7 @@ export default function updaterExtension(pi: ExtensionAPI): void {
140
142
  if (isVersionSkipped(result.latestVersion)) return;
141
143
 
142
144
  // Emit available event
143
- emitEvent(pi as any, UNIPI_EVENTS.UPDATE_AVAILABLE, {
145
+ emitEvent(pi, UNIPI_EVENTS.UPDATE_AVAILABLE, {
144
146
  currentVersion: result.currentVersion,
145
147
  latestVersion: result.latestVersion,
146
148
  });
package/src/installer.ts CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { exec } from "child_process";
9
9
  import { promisify } from "util";
10
- import { getPackageVersion, emitEvent, UNIPI_EVENTS } from "@pi-unipi/core";
10
+ import { getInstalledPackageVersion, emitEvent, UNIPI_EVENTS } from "@pi-unipi/core";
11
11
  import type { InstallResult } from "../types.js";
12
12
 
13
13
  const execAsync = promisify(exec);
@@ -23,9 +23,8 @@ const INSTALL_TIMEOUT_MS = 60000;
23
23
  export async function installUpdate(
24
24
  pi?: { events: { emit: (name: string, payload: unknown) => void } },
25
25
  ): Promise<InstallResult> {
26
- const installedBefore = getPackageVersion(
27
- new URL("../../..", import.meta.url).pathname,
28
- );
26
+ const thisDir = new URL("..", import.meta.url).pathname;
27
+ const installedBefore = getInstalledPackageVersion(thisDir, "@pi-unipi/unipi");
29
28
 
30
29
  try {
31
30
  const { stdout, stderr } = await execAsync(
@@ -37,9 +36,7 @@ export async function installUpdate(
37
36
  );
38
37
 
39
38
  // Get new version after install
40
- const installedAfter = getPackageVersion(
41
- new URL("../../..", import.meta.url).pathname,
42
- );
39
+ const installedAfter = getInstalledPackageVersion(thisDir, "@pi-unipi/unipi");
43
40
 
44
41
  const result: InstallResult = {
45
42
  success: true,
@@ -55,8 +52,11 @@ export async function installUpdate(
55
52
  }
56
53
 
57
54
  return result;
58
- } catch (err: any) {
59
- const errorMessage = err.stderr || err.message || "Unknown install error";
55
+ } catch (err: unknown) {
56
+ const errorMessage = (err instanceof Error && 'stderr' in err ? String((err as Error & { stderr?: string }).stderr) : undefined)
57
+ || (err instanceof Error ? err.message : undefined)
58
+ || String(err)
59
+ || "Unknown install error";
60
60
 
61
61
  // Emit error event
62
62
  if (pi) {
package/src/readme.ts CHANGED
@@ -7,7 +7,8 @@
7
7
  */
8
8
 
9
9
  import { existsSync, readFileSync } from "fs";
10
- import { join, resolve } from "path";
10
+ import { join } from "path";
11
+ import { findPackageRoot } from "@pi-unipi/core";
11
12
  import { MODULES } from "@pi-unipi/core";
12
13
  import type { ReadmeEntry } from "../types.js";
13
14
 
@@ -35,14 +36,12 @@ const PACKAGE_MAP: Record<string, string> = {
35
36
  updater: MODULES.UPDATER,
36
37
  };
37
38
 
38
- /** Resolve the unipi monorepo root directory */
39
+ /** Resolve the unipi package root directory */
39
40
  function resolveUnipiRoot(): string {
40
- // Walk up from this file to find the monorepo root (has package.json with @pi-unipi/unipi)
41
- let dir = new URL(".", import.meta.url).pathname;
42
- // From src/updater/src/ go up 4 levels to reach monorepo root
43
- // Actually: packages/updater/src/readme.ts ../../.. = monorepo root
44
- dir = resolve(dir, "../../..");
45
- return dir;
41
+ // Walk up from this file to find @pi-unipi/unipi by name
42
+ const dir = new URL(".", import.meta.url).pathname;
43
+ const root = findPackageRoot(dir, "@pi-unipi/unipi");
44
+ return root ?? join(dir, ".."); // fallback to parent
46
45
  }
47
46
 
48
47
  /** Get version from a package.json at the given directory */
@@ -11,7 +11,7 @@ import { Key, matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi
11
11
  import type { Theme } from "@mariozechner/pi-coding-agent";
12
12
  import { parseChangelog } from "../changelog.js";
13
13
  import { renderMarkdown } from "../markdown.js";
14
- import { getPackageVersion } from "@pi-unipi/core";
14
+ import { getInstalledPackageVersion } from "@pi-unipi/core";
15
15
  import type { ChangelogEntry } from "../../types.js";
16
16
 
17
17
  type View = "list" | "detail";
@@ -39,13 +39,14 @@ function padVisible(content: string, targetWidth: number): string {
39
39
  */
40
40
  export function renderChangelogOverlay() {
41
41
  return (
42
- tui: any,
42
+ tui: import("@mariozechner/pi-tui").TUI,
43
43
  theme: Theme,
44
- _kb: any,
44
+ _kb: import("@mariozechner/pi-coding-agent").KeybindingsManager,
45
45
  done: (result: { viewed: boolean } | null) => void,
46
46
  ) => {
47
- const installedVersion = getPackageVersion(
48
- new URL("../../..", import.meta.url).pathname,
47
+ const installedVersion = getInstalledPackageVersion(
48
+ new URL("..", import.meta.url).pathname,
49
+ "@pi-unipi/unipi",
49
50
  );
50
51
 
51
52
  const state: ChangelogState = {
@@ -38,9 +38,9 @@ function padVisible(content: string, targetWidth: number): string {
38
38
  */
39
39
  export function renderReadmeOverlay(params?: { openDirect?: string }) {
40
40
  return (
41
- tui: any,
41
+ tui: import("@mariozechner/pi-tui").TUI,
42
42
  theme: Theme,
43
- _kb: any,
43
+ _kb: import("@mariozechner/pi-coding-agent").KeybindingsManager,
44
44
  done: (result: { viewed: boolean } | null) => void,
45
45
  ) => {
46
46
  const state: ReadmeState = {
@@ -61,7 +61,7 @@ export function renderReadmeOverlay(params?: { openDirect?: string }) {
61
61
  const readmePath = resolveReadmePath(params.openDirect);
62
62
  if (readmePath) {
63
63
  const content = readFileSync(readmePath, "utf-8");
64
- state.contentLines = renderMarkdown(content, (tui.width ?? 80) - 4, theme);
64
+ state.contentLines = renderMarkdown(content, (tui.terminal?.columns ?? 80) - 4, theme);
65
65
  state.view = "content";
66
66
  }
67
67
  }
@@ -203,7 +203,7 @@ export function renderReadmeOverlay(params?: { openDirect?: string }) {
203
203
  const entry = state.entries[state.listIndex]!;
204
204
  try {
205
205
  const content = readFileSync(entry.path, "utf-8");
206
- state.contentLines = renderMarkdown(content, (tui.width ?? 80) - 4, theme);
206
+ state.contentLines = renderMarkdown(content, (tui.terminal?.columns ?? 80) - 4, theme);
207
207
  state.contentScroll = 0;
208
208
  state.view = "content";
209
209
  } catch {
@@ -42,9 +42,9 @@ function trunc(text: string, width: number): string {
42
42
  */
43
43
  export function renderSettingsOverlay() {
44
44
  return (
45
- tui: any,
46
- _theme: any,
47
- _kb: any,
45
+ tui: import("@mariozechner/pi-tui").TUI,
46
+ _theme: import("@mariozechner/pi-coding-agent").Theme,
47
+ _kb: import("@mariozechner/pi-coding-agent").KeybindingsManager,
48
48
  done: (result: { saved: boolean } | null) => void,
49
49
  ) => {
50
50
  const state = {
@@ -41,9 +41,9 @@ interface UpdateState {
41
41
  */
42
42
  export function renderUpdateOverlay(checkResult: UpdateCheckResult) {
43
43
  return (
44
- tui: any,
44
+ tui: import("@mariozechner/pi-tui").TUI,
45
45
  theme: Theme,
46
- _kb: any,
46
+ _kb: import("@mariozechner/pi-coding-agent").KeybindingsManager,
47
47
  done: (result: { updated: boolean } | null) => void,
48
48
  ) => {
49
49
  const config = loadConfig();
@@ -66,7 +66,7 @@ export function renderUpdateOverlay(checkResult: UpdateCheckResult) {
66
66
  : `${theme.bold(entry.version)} — ${theme.fg("muted", "Unreleased")}`;
67
67
  contentLines.push(` ${title}`);
68
68
  // Use markdown renderer for the body content
69
- const bodyLines = renderMarkdown(entry.body, (tui.width ?? 80) - 6, theme);
69
+ const bodyLines = renderMarkdown(entry.body, (tui.terminal?.columns ?? 80) - 6, theme);
70
70
  for (const line of bodyLines) {
71
71
  contentLines.push(` ${line}`);
72
72
  }