@pi-unipi/updater 0.1.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/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # @pi-unipi/updater
2
+
3
+ Auto-updater, changelog browser, and readme browser for the Unipi extension suite.
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
10
+
11
+ ## Commands
12
+
13
+ | Command | Description |
14
+ |---------|-------------|
15
+ | `/unipi:readme [package]` | Browse package README files. No arg opens list, with arg opens directly. |
16
+ | `/unipi:changelog` | Browse CHANGELOG.md with version list and detail view |
17
+ | `/unipi:updater-settings` | Configure check interval and auto-update mode |
18
+
19
+ ## Configuration
20
+
21
+ Config stored at `~/.unipi/config/updater/config.json`:
22
+
23
+ ```json
24
+ {
25
+ "checkIntervalMs": 3600000,
26
+ "autoUpdate": "notify"
27
+ }
28
+ ```
29
+
30
+ ### Options
31
+
32
+ | Option | Values | Default |
33
+ |--------|--------|---------|
34
+ | `checkIntervalMs` | `1800000` (30min), `3600000` (1h), `21600000` (6h), `86400000` (1d) | `3600000` (1h) |
35
+ | `autoUpdate` | `disabled`, `notify`, `auto` | `notify` |
36
+
37
+ ### Auto-update modes
38
+
39
+ - **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`
51
+
52
+ ## TUI Overlays
53
+
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)
61
+
62
+ ## Cache
63
+
64
+ Last-check cache stored at `~/.unipi/cache/updater/last-check.json`:
65
+ ```json
66
+ {
67
+ "lastCheck": "2026-05-01T12:00:00.000Z",
68
+ "latestVersion": "0.1.16",
69
+ "skippedVersion": "0.1.16"
70
+ }
71
+ ```
package/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @pi-unipi/updater — Re-export entry
3
+ */
4
+
5
+ export { default } from "./src/index.js";
6
+ export type * from "./types.js";
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@pi-unipi/updater",
3
+ "version": "0.1.1",
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
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Neuron Mr White",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Neuron-Mr-White/unipi.git",
11
+ "directory": "packages/updater"
12
+ },
13
+ "keywords": [
14
+ "pi-package",
15
+ "pi-extension",
16
+ "pi-coding-agent",
17
+ "unipi",
18
+ "updater",
19
+ "changelog",
20
+ "readme"
21
+ ],
22
+ "files": [
23
+ "index.ts",
24
+ "src/**/*.ts",
25
+ "skills/**/*",
26
+ "README.md"
27
+ ],
28
+ "pi": {
29
+ "extensions": [
30
+ "src/index.ts"
31
+ ],
32
+ "skills": [
33
+ "skills"
34
+ ]
35
+ },
36
+ "scripts": {
37
+ "test": "node --experimental-strip-types --test tests/**/*.test.ts"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "dependencies": {
43
+ "@pi-unipi/core": "*"
44
+ },
45
+ "peerDependencies": {
46
+ "@mariozechner/pi-coding-agent": "*",
47
+ "@mariozechner/pi-tui": "*"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^25.6.0",
51
+ "typescript": "^6.0.0"
52
+ }
53
+ }
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: configure-updater
3
+ description: Guide for using and configuring the Unipi updater — auto-update, changelog browser, and readme browser.
4
+ ---
5
+
6
+ # Configure Updater
7
+
8
+ The `@pi-unipi/updater` package provides auto-update checking, a changelog browser, and a readme browser.
9
+
10
+ ## Commands
11
+
12
+ ### `/unipi:readme [package]`
13
+
14
+ Browse README.md files for all unipi packages.
15
+
16
+ - **No argument** — Opens a list of all packages with their versions. Select one to read its README.
17
+ - **With argument** (e.g., `/unipi:readme workflow`) — Opens that package's README directly.
18
+
19
+ ### `/unipi:changelog`
20
+
21
+ Browse the root CHANGELOG.md in a TUI overlay.
22
+
23
+ - Shows version list with dates and status labels (✓ Current, ↑ New)
24
+ - Select a version to view its changelog details (Added, Fixed, Changed sections)
25
+ - Follows [Keep a Changelog](https://keepachangelog.com) format
26
+
27
+ ### `/unipi:updater-settings`
28
+
29
+ Configure the updater module.
30
+
31
+ **Settings:**
32
+ - **Check Interval** — How often to check npm for updates (30min / 1h / 6h / 1d)
33
+ - **Auto Update** — What happens when an update is found:
34
+ - `disabled` — No update checks on session start
35
+ - `notify` — Show update overlay, user chooses to update or skip
36
+ - `auto` — Auto-install after 5-second countdown (press `n` to cancel)
37
+
38
+ ## Config File
39
+
40
+ Config stored at `~/.unipi/config/updater/config.json`:
41
+
42
+ ```json
43
+ {
44
+ "checkIntervalMs": 3600000,
45
+ "autoUpdate": "notify"
46
+ }
47
+ ```
48
+
49
+ ## Cache
50
+
51
+ Last-check cache at `~/.unipi/cache/updater/last-check.json`:
52
+ - Tracks when the last npm check was performed
53
+ - Stores the latest version found
54
+ - Remembers skipped versions (user pressed `n` to skip)
55
+ - Re-prompts only when a newer version than the skipped one appears
56
+
57
+ ## Navigation
58
+
59
+ All TUI overlays support:
60
+ - `j`/`k` or ↑/↓ — Navigate
61
+ - `Enter` — Select/open
62
+ - `q`/`Esc` — Back/close
63
+ - `g`/`G` — Jump to top/bottom
64
+ - `Space` — Cycle options (settings)
65
+ - `h`/`l` or ←/→ — Cycle options (settings)
package/src/cache.ts ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @pi-unipi/updater — Last-check cache
3
+ *
4
+ * Reads/writes last-check.json to ~/.unipi/cache/updater/last-check.json
5
+ * Tracks when the last npm check was performed and what version was found.
6
+ */
7
+
8
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
9
+ import { dirname, join } from "path";
10
+ import { homedir } from "os";
11
+ import { UPDATER_DIRS } from "@pi-unipi/core";
12
+ import type { LastCheckCache } from "../types.js";
13
+
14
+ /** Resolve cache path */
15
+ function resolveCachePath(): string {
16
+ const base = UPDATER_DIRS.CACHE.replace("~", homedir());
17
+ return join(base, "last-check.json");
18
+ }
19
+
20
+ /** Read cache from disk, returning null if missing or invalid */
21
+ export function readLastCheck(): LastCheckCache | null {
22
+ const cachePath = resolveCachePath();
23
+ try {
24
+ if (existsSync(cachePath)) {
25
+ const raw = readFileSync(cachePath, "utf-8");
26
+ const parsed = JSON.parse(raw) as LastCheckCache;
27
+ if (parsed.lastCheck && parsed.latestVersion) {
28
+ return parsed;
29
+ }
30
+ }
31
+ } catch (_err) {
32
+ // Cache read failure — treat as missing.
33
+ }
34
+ return null;
35
+ }
36
+
37
+ /** Write cache to disk, creating directory if needed */
38
+ export function writeLastCheck(cache: LastCheckCache): void {
39
+ const cachePath = resolveCachePath();
40
+ const dir = dirname(cachePath);
41
+ mkdirSync(dir, { recursive: true });
42
+ writeFileSync(cachePath, JSON.stringify(cache, null, 2) + "\n", "utf-8");
43
+ }
44
+
45
+ /** Check if enough time has elapsed since last check */
46
+ export function isCheckDue(intervalMs: number): boolean {
47
+ const cache = readLastCheck();
48
+ if (!cache) return true;
49
+
50
+ const lastCheckTime = new Date(cache.lastCheck).getTime();
51
+ const now = Date.now();
52
+ return now - lastCheckTime >= intervalMs;
53
+ }
54
+
55
+ /** Write skipped version to cache */
56
+ export function writeSkippedVersion(version: string): void {
57
+ const cache = readLastCheck();
58
+ if (cache) {
59
+ writeLastCheck({ ...cache, skippedVersion: version });
60
+ }
61
+ }
62
+
63
+ /** Check if a version was skipped by the user */
64
+ export function isVersionSkipped(version: string): boolean {
65
+ const cache = readLastCheck();
66
+ return cache?.skippedVersion === version;
67
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * @pi-unipi/updater — Changelog parser
3
+ *
4
+ * Parses CHANGELOG.md (Keep a Changelog format) into structured ChangelogEntry[].
5
+ * Handles: ## [Unreleased], ## [x.y.z] — YYYY-MM-DD, ### Added/Fixed/etc.
6
+ */
7
+
8
+ import { existsSync, readFileSync } from "fs";
9
+ import type { ChangelogEntry } from "../types.js";
10
+
11
+ /** Regex for version headers: ## [x.y.z] — YYYY-MM-DD or ## [Unreleased] */
12
+ const VERSION_HEADER_RE = /^## \[(.+?)\](?:\s*[-—–]\s*(.+))?$/;
13
+
14
+ /** Regex for section headers: ### Added, ### Fixed, etc. */
15
+ const SECTION_HEADER_RE = /^### (.+)$/;
16
+
17
+ /**
18
+ * Parse a CHANGELOG.md file into structured entries.
19
+ * Returns empty array if file doesn't exist or is empty.
20
+ */
21
+ export function parseChangelog(filePath: string): ChangelogEntry[] {
22
+ if (!existsSync(filePath)) return [];
23
+
24
+ const content = readFileSync(filePath, "utf-8").trim();
25
+ if (!content) return [];
26
+
27
+ const lines = content.split("\n");
28
+ const entries: ChangelogEntry[] = [];
29
+
30
+ let currentEntry: ChangelogEntry | null = null;
31
+ let currentSection: string | null = null;
32
+ let currentItems: string[] = [];
33
+
34
+ for (const line of lines) {
35
+ const versionMatch = line.match(VERSION_HEADER_RE);
36
+ if (versionMatch) {
37
+ // Save previous entry
38
+ if (currentEntry) {
39
+ if (currentSection && currentItems.length > 0) {
40
+ currentEntry.sections[currentSection] = currentItems;
41
+ }
42
+ entries.push(currentEntry);
43
+ }
44
+
45
+ // Start new entry
46
+ const version = versionMatch[1].trim();
47
+ const date = (versionMatch[2] ?? "").trim();
48
+ currentEntry = {
49
+ version,
50
+ date,
51
+ sections: {},
52
+ body: "",
53
+ };
54
+ currentSection = null;
55
+ currentItems = [];
56
+ continue;
57
+ }
58
+
59
+ if (!currentEntry) continue;
60
+
61
+ const sectionMatch = line.match(SECTION_HEADER_RE);
62
+ if (sectionMatch) {
63
+ // Save previous section
64
+ if (currentSection && currentItems.length > 0) {
65
+ currentEntry.sections[currentSection] = currentItems;
66
+ }
67
+ currentSection = sectionMatch[1].trim();
68
+ currentItems = [];
69
+ continue;
70
+ }
71
+
72
+ // Collect section items (lines starting with - or *)
73
+ const trimmed = line.trim();
74
+ if (currentSection && (trimmed.startsWith("- ") || trimmed.startsWith("* "))) {
75
+ currentItems.push(trimmed.slice(2).trim());
76
+ } else if (trimmed && currentSection) {
77
+ // Continuation of previous item
78
+ if (currentItems.length > 0) {
79
+ currentItems[currentItems.length - 1] += " " + trimmed;
80
+ }
81
+ }
82
+ }
83
+
84
+ // Save last entry
85
+ if (currentEntry) {
86
+ if (currentSection && currentItems.length > 0) {
87
+ currentEntry.sections[currentSection] = currentItems;
88
+ }
89
+ entries.push(currentEntry);
90
+ }
91
+
92
+ // Build body text for each entry (for rendering)
93
+ for (const entry of entries) {
94
+ const lines: string[] = [];
95
+ for (const [section, items] of Object.entries(entry.sections)) {
96
+ lines.push(`### ${section}`);
97
+ for (const item of items) {
98
+ lines.push(`- ${item}`);
99
+ }
100
+ lines.push("");
101
+ }
102
+ entry.body = lines.join("\n").trim();
103
+ }
104
+
105
+ return entries;
106
+ }
107
+
108
+ /**
109
+ * Get changelog entries for versions newer than the given version.
110
+ * Returns entries in newest-first order, excluding the given version.
111
+ */
112
+ export function getNewerVersions(
113
+ entries: ChangelogEntry[],
114
+ installedVersion: string,
115
+ ): ChangelogEntry[] {
116
+ const result: ChangelogEntry[] = [];
117
+ for (const entry of entries) {
118
+ if (entry.version === "Unreleased") {
119
+ result.push(entry);
120
+ continue;
121
+ }
122
+ if (entry.version === installedVersion) break;
123
+ result.push(entry);
124
+ }
125
+ return result;
126
+ }
127
+
128
+ /**
129
+ * Compare semver strings (simple lexicographic for x.y.z format).
130
+ * Returns positive if a > b, negative if a < b, 0 if equal.
131
+ */
132
+ export function compareVersions(a: string, b: string): number {
133
+ const pa = a.split(".").map(Number);
134
+ const pb = b.split(".").map(Number);
135
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
136
+ const na = pa[i] ?? 0;
137
+ const nb = pb[i] ?? 0;
138
+ if (na !== nb) return na - nb;
139
+ }
140
+ return 0;
141
+ }
package/src/checker.ts ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * @pi-unipi/updater — NPM registry checker
3
+ *
4
+ * Fetches latest version from npm registry, compares with installed version,
5
+ * respects check interval from config/cache.
6
+ */
7
+
8
+ import { getPackageVersion } from "@pi-unipi/core";
9
+ import { loadConfig } from "./settings.js";
10
+ import { readLastCheck, writeLastCheck, isCheckDue } from "./cache.js";
11
+ import type { UpdateCheckResult } from "../types.js";
12
+
13
+ /** NPM registry URL for the unipi umbrella package */
14
+ const NPM_REGISTRY_URL = "https://registry.npmjs.org/@pi-unipi/unipi";
15
+
16
+ /** Resolve the installed version of @pi-unipi/unipi */
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);
21
+ }
22
+
23
+ /**
24
+ * Check for updates from npm registry.
25
+ * Respects check interval — skips if last check was recent.
26
+ * Returns update status and version info.
27
+ */
28
+ export async function checkForUpdates(): Promise<UpdateCheckResult> {
29
+ const currentVersion = getInstalledVersion();
30
+
31
+ try {
32
+ const config = loadConfig();
33
+
34
+ // Check if we need to fetch (interval not elapsed)
35
+ if (!isCheckDue(config.checkIntervalMs)) {
36
+ const cache = readLastCheck();
37
+ if (cache) {
38
+ return {
39
+ updateAvailable: cache.latestVersion !== currentVersion,
40
+ latestVersion: cache.latestVersion,
41
+ currentVersion,
42
+ };
43
+ }
44
+ }
45
+
46
+ // Fetch from npm registry
47
+ const response = await fetch(NPM_REGISTRY_URL, {
48
+ headers: { Accept: "application/json" },
49
+ signal: AbortSignal.timeout(10000), // 10s timeout
50
+ });
51
+
52
+ if (!response.ok) {
53
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
54
+ }
55
+
56
+ const data = await response.json() as { "dist-tags": { latest: string } };
57
+ const latestVersion = data["dist-tags"]?.latest;
58
+
59
+ if (!latestVersion) {
60
+ throw new Error("No dist-tags.latest in npm response");
61
+ }
62
+
63
+ // Write cache
64
+ writeLastCheck({
65
+ lastCheck: new Date().toISOString(),
66
+ latestVersion,
67
+ });
68
+
69
+ return {
70
+ updateAvailable: latestVersion !== currentVersion,
71
+ latestVersion,
72
+ currentVersion,
73
+ };
74
+ } catch (err: any) {
75
+ // Network error — return cached info if available
76
+ const cache = readLastCheck();
77
+ return {
78
+ updateAvailable: false,
79
+ latestVersion: cache?.latestVersion ?? "",
80
+ currentVersion,
81
+ error: err.message ?? "Unknown error",
82
+ };
83
+ }
84
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * @pi-unipi/updater — Command Registration
3
+ *
4
+ * Registers /unipi:readme [package], /unipi:changelog, /unipi:updater-settings
5
+ */
6
+
7
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
8
+ import { UNIPI_PREFIX, UPDATER_COMMANDS } from "@pi-unipi/core";
9
+ import { renderReadmeOverlay } from "./tui/readme-overlay.js";
10
+ import { renderChangelogOverlay } from "./tui/changelog-overlay.js";
11
+ import { renderSettingsOverlay } from "./tui/settings-overlay.js";
12
+
13
+ /** Common overlay options for all updater overlays */
14
+ const OVERLAY_OPTIONS = {
15
+ overlay: true,
16
+ overlayOptions: {
17
+ width: "80%",
18
+ minWidth: 60,
19
+ anchor: "center",
20
+ margin: 2,
21
+ },
22
+ };
23
+
24
+ /** Register updater commands */
25
+ export function registerCommands(pi: ExtensionAPI): void {
26
+ // /unipi:readme [package] — Open readme browser
27
+ pi.registerCommand(
28
+ `${UNIPI_PREFIX}${UPDATER_COMMANDS.README}`,
29
+ {
30
+ description: "Browse package README files",
31
+ handler: async (args: string, ctx: any) => {
32
+ const packageName = args.trim() || undefined;
33
+ try {
34
+ await ctx.ui.custom(
35
+ renderReadmeOverlay({ openDirect: packageName }),
36
+ OVERLAY_OPTIONS,
37
+ );
38
+ } catch (err) {
39
+ ctx.ui.notify(`Readme overlay error: ${err}`, "error");
40
+ }
41
+ },
42
+ },
43
+ );
44
+
45
+ // /unipi:changelog — Open changelog browser
46
+ pi.registerCommand(
47
+ `${UNIPI_PREFIX}${UPDATER_COMMANDS.CHANGELOG}`,
48
+ {
49
+ description: "Browse changelog (Keep a Changelog format)",
50
+ handler: async (_args: string, ctx: any) => {
51
+ try {
52
+ await ctx.ui.custom(
53
+ renderChangelogOverlay(),
54
+ OVERLAY_OPTIONS,
55
+ );
56
+ } catch (err) {
57
+ ctx.ui.notify(`Changelog overlay error: ${err}`, "error");
58
+ }
59
+ },
60
+ },
61
+ );
62
+
63
+ // /unipi:updater-settings — Open updater settings
64
+ pi.registerCommand(
65
+ `${UNIPI_PREFIX}${UPDATER_COMMANDS.UPDATER_SETTINGS}`,
66
+ {
67
+ description: "Configure updater — check interval and auto-update mode",
68
+ handler: async (_args: string, ctx: any) => {
69
+ try {
70
+ const result = await ctx.ui.custom(
71
+ renderSettingsOverlay(),
72
+ OVERLAY_OPTIONS,
73
+ );
74
+ if (result?.saved) {
75
+ ctx.ui.notify("Updater settings saved.", "info");
76
+ }
77
+ } catch (err) {
78
+ ctx.ui.notify(`Settings overlay error: ${err}`, "error");
79
+ }
80
+ },
81
+ },
82
+ );
83
+ }