@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 +71 -0
- package/index.ts +6 -0
- package/package.json +53 -0
- package/skills/configure-updater/SKILL.md +65 -0
- package/src/cache.ts +67 -0
- package/src/changelog.ts +141 -0
- package/src/checker.ts +84 -0
- package/src/commands.ts +83 -0
- package/src/index.ts +178 -0
- package/src/installer.ts +74 -0
- package/src/markdown.ts +173 -0
- package/src/readme.ts +139 -0
- package/src/settings.ts +98 -0
- package/src/tui/changelog-overlay.ts +256 -0
- package/src/tui/readme-overlay.ts +236 -0
- package/src/tui/settings-overlay.ts +191 -0
- package/src/tui/update-overlay.ts +261 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/updater — Update Available TUI Overlay
|
|
3
|
+
*
|
|
4
|
+
* Shows when an update is found on session start.
|
|
5
|
+
* Displays version diff, inline changelog for newer versions,
|
|
6
|
+
* [Y] Update / [n] Skip prompt. Auto mode: countdown with cancel.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { Key, matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
11
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
12
|
+
import { parseChangelog, getNewerVersions } from "../changelog.js";
|
|
13
|
+
import { renderMarkdown } from "../markdown.js";
|
|
14
|
+
import { installUpdate } from "../installer.js";
|
|
15
|
+
import { writeSkippedVersion } from "../cache.js";
|
|
16
|
+
import { loadConfig } from "../settings.js";
|
|
17
|
+
import type { ChangelogEntry, UpdateCheckResult } from "../../types.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Pad content to exact visible width.
|
|
21
|
+
*/
|
|
22
|
+
function padVisible(content: string, targetWidth: number): string {
|
|
23
|
+
const vw = visibleWidth(content);
|
|
24
|
+
const pad = Math.max(0, targetWidth - vw);
|
|
25
|
+
return content + " ".repeat(pad);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface UpdateState {
|
|
29
|
+
result: UpdateCheckResult;
|
|
30
|
+
contentLines: string[];
|
|
31
|
+
scroll: number;
|
|
32
|
+
installing: boolean;
|
|
33
|
+
installError: string | null;
|
|
34
|
+
autoCountdown: number;
|
|
35
|
+
autoCancelled: boolean;
|
|
36
|
+
autoTimer: ReturnType<typeof setInterval> | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Render the update available overlay.
|
|
41
|
+
*/
|
|
42
|
+
export function renderUpdateOverlay(checkResult: UpdateCheckResult) {
|
|
43
|
+
return (
|
|
44
|
+
tui: any,
|
|
45
|
+
theme: Theme,
|
|
46
|
+
_kb: any,
|
|
47
|
+
done: (result: { updated: boolean } | null) => void,
|
|
48
|
+
) => {
|
|
49
|
+
const config = loadConfig();
|
|
50
|
+
|
|
51
|
+
// Load changelog for newer versions
|
|
52
|
+
let newerVersions: ChangelogEntry[] = [];
|
|
53
|
+
const changelogPath = join(process.cwd(), "CHANGELOG.md");
|
|
54
|
+
try {
|
|
55
|
+
const entries = parseChangelog(changelogPath);
|
|
56
|
+
newerVersions = getNewerVersions(entries, checkResult.currentVersion);
|
|
57
|
+
} catch (_err) {
|
|
58
|
+
// No changelog
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Build content lines from changelog using markdown renderer
|
|
62
|
+
const contentLines: string[] = [];
|
|
63
|
+
for (const entry of newerVersions) {
|
|
64
|
+
const title = entry.date
|
|
65
|
+
? `${theme.bold(entry.version)} — ${theme.fg("muted", entry.date)}`
|
|
66
|
+
: `${theme.bold(entry.version)} — ${theme.fg("muted", "Unreleased")}`;
|
|
67
|
+
contentLines.push(` ${title}`);
|
|
68
|
+
// Use markdown renderer for the body content
|
|
69
|
+
const bodyLines = renderMarkdown(entry.body, (tui.width ?? 80) - 6, theme);
|
|
70
|
+
for (const line of bodyLines) {
|
|
71
|
+
contentLines.push(` ${line}`);
|
|
72
|
+
}
|
|
73
|
+
contentLines.push("");
|
|
74
|
+
}
|
|
75
|
+
if (contentLines.length === 0) {
|
|
76
|
+
contentLines.push(` ${theme.fg("muted", "No changelog available for this update.")}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const state: UpdateState = {
|
|
80
|
+
result: checkResult,
|
|
81
|
+
contentLines,
|
|
82
|
+
scroll: 0,
|
|
83
|
+
installing: false,
|
|
84
|
+
installError: null,
|
|
85
|
+
autoCountdown: 5,
|
|
86
|
+
autoCancelled: false,
|
|
87
|
+
autoTimer: null,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Auto mode countdown
|
|
91
|
+
if (config.autoUpdate === "auto") {
|
|
92
|
+
state.autoTimer = setInterval(() => {
|
|
93
|
+
if (state.autoCancelled || state.installing) {
|
|
94
|
+
if (state.autoTimer) clearInterval(state.autoTimer);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
state.autoCountdown--;
|
|
98
|
+
if (state.autoCountdown <= 0) {
|
|
99
|
+
if (state.autoTimer) clearInterval(state.autoTimer);
|
|
100
|
+
doInstall();
|
|
101
|
+
}
|
|
102
|
+
tui.requestRender();
|
|
103
|
+
}, 1000);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const doInstall = async () => {
|
|
107
|
+
if (state.installing) return;
|
|
108
|
+
state.installing = true;
|
|
109
|
+
tui.requestRender();
|
|
110
|
+
|
|
111
|
+
const result = await installUpdate();
|
|
112
|
+
if (result.success) {
|
|
113
|
+
done({ updated: true });
|
|
114
|
+
} else {
|
|
115
|
+
state.installing = false;
|
|
116
|
+
state.installError = result.error ?? "Installation failed";
|
|
117
|
+
tui.requestRender();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const render = (width: number): string[] => {
|
|
122
|
+
const innerWidth = Math.max(22, width - 2);
|
|
123
|
+
const lines: string[] = [];
|
|
124
|
+
|
|
125
|
+
// ── Header ──────────────────────────────────────────────────────
|
|
126
|
+
lines.push(theme.fg("accent", `╭${"─".repeat(innerWidth)}╮`));
|
|
127
|
+
lines.push(
|
|
128
|
+
theme.fg("accent", "│") +
|
|
129
|
+
padVisible(theme.fg("accent", theme.bold(" 📦 Update Available ")), innerWidth) +
|
|
130
|
+
theme.fg("accent", "│"),
|
|
131
|
+
);
|
|
132
|
+
lines.push(theme.fg("accent", `├${"─".repeat(innerWidth)}┤`));
|
|
133
|
+
|
|
134
|
+
// ── Version info ────────────────────────────────────────────────
|
|
135
|
+
const current = theme.fg("muted", state.result.currentVersion);
|
|
136
|
+
const latest = theme.fg("success", theme.bold(state.result.latestVersion));
|
|
137
|
+
lines.push(
|
|
138
|
+
theme.fg("accent", "│") +
|
|
139
|
+
padVisible(truncateToWidth(` ${current} → ${latest}`, innerWidth), innerWidth) +
|
|
140
|
+
theme.fg("accent", "│"),
|
|
141
|
+
);
|
|
142
|
+
lines.push(
|
|
143
|
+
theme.fg("accent", "│") +
|
|
144
|
+
padVisible("", innerWidth) +
|
|
145
|
+
theme.fg("accent", "│"),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// ── Changelog content (scrollable) ──────────────────────────────
|
|
149
|
+
const contentHeight = 12;
|
|
150
|
+
const maxScroll = Math.max(0, state.contentLines.length - contentHeight);
|
|
151
|
+
state.scroll = Math.min(state.scroll, maxScroll);
|
|
152
|
+
state.scroll = Math.max(0, state.scroll);
|
|
153
|
+
|
|
154
|
+
const visible = state.contentLines.slice(state.scroll, state.scroll + contentHeight);
|
|
155
|
+
for (let i = 0; i < contentHeight; i++) {
|
|
156
|
+
const line = visible[i] ?? "";
|
|
157
|
+
lines.push(
|
|
158
|
+
theme.fg("accent", "│") +
|
|
159
|
+
padVisible(truncateToWidth(line, innerWidth), innerWidth) +
|
|
160
|
+
theme.fg("accent", "│"),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Action bar ──────────────────────────────────────────────────
|
|
165
|
+
lines.push(theme.fg("accent", `├${"─".repeat(innerWidth)}┤`));
|
|
166
|
+
|
|
167
|
+
if (state.installing) {
|
|
168
|
+
lines.push(
|
|
169
|
+
theme.fg("accent", "│") +
|
|
170
|
+
padVisible(theme.fg("warning", " Installing update..."), innerWidth) +
|
|
171
|
+
theme.fg("accent", "│"),
|
|
172
|
+
);
|
|
173
|
+
} else if (state.installError) {
|
|
174
|
+
lines.push(
|
|
175
|
+
theme.fg("accent", "│") +
|
|
176
|
+
padVisible(theme.fg("error", ` Error: ${state.installError}`), innerWidth) +
|
|
177
|
+
theme.fg("accent", "│"),
|
|
178
|
+
);
|
|
179
|
+
lines.push(
|
|
180
|
+
theme.fg("accent", "│") +
|
|
181
|
+
padVisible(theme.fg("muted", " Press any key to dismiss"), innerWidth) +
|
|
182
|
+
theme.fg("accent", "│"),
|
|
183
|
+
);
|
|
184
|
+
} else if (config.autoUpdate === "auto" && !state.autoCancelled) {
|
|
185
|
+
const actionLine = ` ${theme.fg("success", "[Y]")} Update now ${theme.fg("muted", "[n]")} Cancel Auto-updating in ${theme.fg("warning", String(state.autoCountdown))}...`;
|
|
186
|
+
lines.push(
|
|
187
|
+
theme.fg("accent", "│") +
|
|
188
|
+
padVisible(truncateToWidth(actionLine, innerWidth), innerWidth) +
|
|
189
|
+
theme.fg("accent", "│"),
|
|
190
|
+
);
|
|
191
|
+
} else {
|
|
192
|
+
const actionLine = ` ${theme.fg("success", "[Y]")} Update now ${theme.fg("muted", "[n]")} Skip ${theme.fg("accent", "j/k")}: scroll`;
|
|
193
|
+
lines.push(
|
|
194
|
+
theme.fg("accent", "│") +
|
|
195
|
+
padVisible(truncateToWidth(actionLine, innerWidth), innerWidth) +
|
|
196
|
+
theme.fg("accent", "│"),
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
lines.push(theme.fg("accent", `╰${"─".repeat(innerWidth)}╯`));
|
|
201
|
+
|
|
202
|
+
return lines;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const handleInput = (data: string) => {
|
|
206
|
+
// Dismiss install error
|
|
207
|
+
if (state.installError) {
|
|
208
|
+
done({ updated: false });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (state.installing) return;
|
|
213
|
+
|
|
214
|
+
// Cancel auto countdown
|
|
215
|
+
if (config.autoUpdate === "auto" && !state.autoCancelled) {
|
|
216
|
+
if (data === "n") {
|
|
217
|
+
state.autoCancelled = true;
|
|
218
|
+
if (state.autoTimer) clearInterval(state.autoTimer);
|
|
219
|
+
writeSkippedVersion(state.result.latestVersion);
|
|
220
|
+
done({ updated: false });
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Y — install
|
|
226
|
+
if (data === "y" || data === "Y") {
|
|
227
|
+
doInstall();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// n — skip
|
|
232
|
+
if (data === "n") {
|
|
233
|
+
writeSkippedVersion(state.result.latestVersion);
|
|
234
|
+
done({ updated: false });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// q/Esc — skip
|
|
239
|
+
if (matchesKey(data, Key.escape) || data === "q") {
|
|
240
|
+
writeSkippedVersion(state.result.latestVersion);
|
|
241
|
+
done({ updated: false });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Scroll
|
|
246
|
+
if (matchesKey(data, Key.down) || data === "j") {
|
|
247
|
+
state.scroll++;
|
|
248
|
+
} else if (matchesKey(data, Key.up) || data === "k") {
|
|
249
|
+
state.scroll = Math.max(0, state.scroll - 1);
|
|
250
|
+
} else if (data === "g") {
|
|
251
|
+
state.scroll = 0;
|
|
252
|
+
} else if (data === "G") {
|
|
253
|
+
state.scroll = 999999;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
tui.requestRender();
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
return { render, handleInput, invalidate: () => {}, focused: true };
|
|
260
|
+
};
|
|
261
|
+
}
|