@gaosh3n/pi-package-manager 0.1.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/LICENSE +21 -0
- package/README.md +46 -0
- package/index.ts +695 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 gaosh3n
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# pi-package-manager
|
|
2
|
+
|
|
3
|
+
`pi-package-manager` keeps your Pi packages up-to-date and shows you what happened, right inside Pi.
|
|
4
|
+
|
|
5
|
+
## What you get
|
|
6
|
+
|
|
7
|
+
- automatic package update checks when Pi starts
|
|
8
|
+
- a visible progress UI while Pi checks or updates packages
|
|
9
|
+
- `/package-manager status` to see whether updates are available
|
|
10
|
+
- `/package-manager update` to run the update flow on demand
|
|
11
|
+
- a final result card in Pi so you can review the latest outcome
|
|
12
|
+
- automatic Pi reload after a successful startup update
|
|
13
|
+
|
|
14
|
+
## Install only this extension
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pi install npm:@gaosh3n/pi-package-manager
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Then restart Pi or run `/reload`.
|
|
21
|
+
|
|
22
|
+
## How to use it
|
|
23
|
+
|
|
24
|
+
### Let it run on startup
|
|
25
|
+
|
|
26
|
+
Start Pi normally. If package updates are available, Pi Package Manager checks for them, shows progress in Pi, and reports the result when it finishes.
|
|
27
|
+
|
|
28
|
+
### Check package status
|
|
29
|
+
|
|
30
|
+
Run:
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
/package-manager status
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
You will get a status card in Pi showing whether updates are available and summarizing the latest package update result.
|
|
37
|
+
|
|
38
|
+
### Run a package update
|
|
39
|
+
|
|
40
|
+
Run:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
/package-manager update
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Pi will run the update flow for you, show live progress, and record the final result in the transcript.
|
package/index.ts
ADDED
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DefaultPackageManager,
|
|
3
|
+
getAgentDir,
|
|
4
|
+
keyText,
|
|
5
|
+
SettingsManager,
|
|
6
|
+
type CustomEntry,
|
|
7
|
+
type ExecResult,
|
|
8
|
+
type ExtensionAPI,
|
|
9
|
+
type ExtensionCommandContext,
|
|
10
|
+
type ExtensionContext,
|
|
11
|
+
type SessionEntry,
|
|
12
|
+
type SessionStartEvent,
|
|
13
|
+
} from "@earendil-works/pi-coding-agent"
|
|
14
|
+
import { type AutocompleteItem, Box, Text } from "@earendil-works/pi-tui"
|
|
15
|
+
|
|
16
|
+
export const AUTO_UPDATE_RECORD_ENTRY_TYPE = "package-manager-auto-update-record"
|
|
17
|
+
export const REPORT_ENTRY_TYPE = "package-manager-report"
|
|
18
|
+
export const PACKAGE_MANAGER_TITLE = "Pi Package Manager"
|
|
19
|
+
|
|
20
|
+
const UPDATE_COMMAND = ["update", "--extensions"] as const
|
|
21
|
+
const PACKAGE_MANAGER_WIDGET_KEY = "pi-package-manager"
|
|
22
|
+
const RELOAD_COUNTDOWN_SECONDS = 5
|
|
23
|
+
const OUTPUT_PREVIEW_LINE_COUNT = 8
|
|
24
|
+
|
|
25
|
+
export type AutoUpdateOutcome = "succeeded" | "failed" | "skipped"
|
|
26
|
+
export type ReportTone = "info" | "success" | "warning" | "error"
|
|
27
|
+
|
|
28
|
+
type WidgetState =
|
|
29
|
+
| { mode: "status-checking" }
|
|
30
|
+
| { mode: "checking" }
|
|
31
|
+
| { mode: "installing"; packages: number }
|
|
32
|
+
| { mode: "countdown"; secondsRemaining: number }
|
|
33
|
+
|
|
34
|
+
export interface AutoUpdateRecord {
|
|
35
|
+
startedAtUtc: string
|
|
36
|
+
endedAtUtc: string
|
|
37
|
+
outcome: AutoUpdateOutcome
|
|
38
|
+
packagesUpdated: number
|
|
39
|
+
reason?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface PackageManagerReport {
|
|
43
|
+
title: string
|
|
44
|
+
headline?: string
|
|
45
|
+
tone: ReportTone
|
|
46
|
+
lines: string[]
|
|
47
|
+
lineTone?: "default" | "dim"
|
|
48
|
+
output?: string
|
|
49
|
+
outputLabel?: string
|
|
50
|
+
outputTone?: "default" | "dim"
|
|
51
|
+
hideOutputWhenCollapsed?: boolean
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface PackageStatusSnapshot {
|
|
55
|
+
availableUpdates: string[]
|
|
56
|
+
lastAutoUpdate?: AutoUpdateRecord
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default function (pi: ExtensionAPI) {
|
|
60
|
+
let lastAutoUpdateRecord: AutoUpdateRecord | undefined
|
|
61
|
+
|
|
62
|
+
pi.registerEntryRenderer<PackageManagerReport>(
|
|
63
|
+
REPORT_ENTRY_TYPE,
|
|
64
|
+
(entry, { expanded }, theme) => {
|
|
65
|
+
const report = entry.data
|
|
66
|
+
|
|
67
|
+
if (!report) {
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const toneColor =
|
|
72
|
+
report.tone === "error"
|
|
73
|
+
? "error"
|
|
74
|
+
: report.tone === "warning"
|
|
75
|
+
? "warning"
|
|
76
|
+
: report.tone === "success"
|
|
77
|
+
? "success"
|
|
78
|
+
: "accent"
|
|
79
|
+
const lineTextTone = report.lineTone === "dim" ? "dim" : "customMessageText"
|
|
80
|
+
const outputTextTone = report.outputTone === "dim" ? "dim" : lineTextTone
|
|
81
|
+
|
|
82
|
+
const box = new Box(1, 1, (text: string) =>
|
|
83
|
+
theme.bg("customMessageBg", text),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
box.addChild(
|
|
87
|
+
new Text(
|
|
88
|
+
`${theme.fg(toneColor, "●")} ${theme.bold(theme.fg("customMessageLabel", report.title))}`,
|
|
89
|
+
0,
|
|
90
|
+
0,
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if (report.headline) {
|
|
95
|
+
box.addChild(new Text(theme.fg("text", report.headline), 0, 0))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (report.lines.length > 0) {
|
|
99
|
+
box.addChild(
|
|
100
|
+
new Text(
|
|
101
|
+
report.lines
|
|
102
|
+
.map((line) => theme.fg(lineTextTone, line))
|
|
103
|
+
.join("\n"),
|
|
104
|
+
0,
|
|
105
|
+
0,
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (report.output?.trim()) {
|
|
111
|
+
if (!expanded && report.hideOutputWhenCollapsed) {
|
|
112
|
+
box.addChild(
|
|
113
|
+
new Text(
|
|
114
|
+
formatExpandHint(theme, "to expand to see update output."),
|
|
115
|
+
0,
|
|
116
|
+
0,
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
} else {
|
|
120
|
+
const { text, truncated } = formatReportOutput(
|
|
121
|
+
report.output,
|
|
122
|
+
expanded,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
box.addChild(
|
|
126
|
+
new Text(
|
|
127
|
+
theme.fg(
|
|
128
|
+
outputTextTone,
|
|
129
|
+
report.outputLabel ?? "Update output:",
|
|
130
|
+
),
|
|
131
|
+
0,
|
|
132
|
+
0,
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
box.addChild(new Text(theme.fg(outputTextTone, text), 0, 0))
|
|
136
|
+
|
|
137
|
+
if (truncated) {
|
|
138
|
+
box.addChild(
|
|
139
|
+
new Text(
|
|
140
|
+
formatExpandHint(
|
|
141
|
+
theme,
|
|
142
|
+
"to expand to view the full update output.",
|
|
143
|
+
),
|
|
144
|
+
0,
|
|
145
|
+
0,
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return box
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
pi.on("session_start", async (event, ctx) => {
|
|
157
|
+
lastAutoUpdateRecord = getLastAutoUpdateRecord(ctx.sessionManager.getEntries())
|
|
158
|
+
|
|
159
|
+
if (!shouldAutoUpdateOnSessionStart(event)) {
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
pi.sendUserMessage("/package-manager update --startup", {
|
|
164
|
+
expandPromptTemplates: true,
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
pi.registerCommand("package-manager", {
|
|
169
|
+
description:
|
|
170
|
+
"Manage Pi package updates (usage: /package-manager [status|update])",
|
|
171
|
+
getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
|
|
172
|
+
const items = ["status", "update"].map((value) => ({
|
|
173
|
+
value,
|
|
174
|
+
label: value,
|
|
175
|
+
}))
|
|
176
|
+
const normalizedPrefix = prefix.trim()
|
|
177
|
+
const filtered = normalizedPrefix
|
|
178
|
+
? items.filter((item) => item.value.startsWith(normalizedPrefix))
|
|
179
|
+
: items
|
|
180
|
+
return filtered.length > 0 ? filtered : null
|
|
181
|
+
},
|
|
182
|
+
handler: async (args, ctx) => {
|
|
183
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean)
|
|
184
|
+
const subcommand = tokens[0] ?? "status"
|
|
185
|
+
|
|
186
|
+
if (subcommand === "status") {
|
|
187
|
+
await handleStatusCommand(pi, ctx, lastAutoUpdateRecord)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (subcommand === "update") {
|
|
192
|
+
const result = await runUpdate(pi, ctx, {
|
|
193
|
+
startupTriggered: tokens.includes("--startup"),
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
lastAutoUpdateRecord = result.autoUpdateRecord
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
ctx.ui.notify("Usage: /package-manager [status|update]", "warning")
|
|
201
|
+
},
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function shouldAutoUpdateOnSessionStart(
|
|
206
|
+
event: Pick<SessionStartEvent, "reason">,
|
|
207
|
+
): boolean {
|
|
208
|
+
return event.reason === "startup"
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function createAutoUpdateRecord(input: {
|
|
212
|
+
startedAtUtc: string
|
|
213
|
+
endedAtUtc: string
|
|
214
|
+
outcome: AutoUpdateOutcome
|
|
215
|
+
packagesUpdated: number
|
|
216
|
+
reason?: string
|
|
217
|
+
}): AutoUpdateRecord {
|
|
218
|
+
return {
|
|
219
|
+
startedAtUtc: input.startedAtUtc,
|
|
220
|
+
endedAtUtc: input.endedAtUtc,
|
|
221
|
+
outcome: input.outcome,
|
|
222
|
+
packagesUpdated: input.packagesUpdated,
|
|
223
|
+
reason: input.reason,
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function formatUtcTimestamp(isoUtc: string): string {
|
|
228
|
+
return isoUtc.replace("T", " ").replace(/\.\d{3}Z$/, " UTC+00")
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function formatStatusLines(snapshot: PackageStatusSnapshot): string[] {
|
|
232
|
+
if (!snapshot.lastAutoUpdate) {
|
|
233
|
+
return ["Latest package update: none recorded."]
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const lines = [
|
|
237
|
+
`Latest update start: ${formatUtcTimestamp(snapshot.lastAutoUpdate.startedAtUtc)}`,
|
|
238
|
+
`Latest update end: ${formatUtcTimestamp(snapshot.lastAutoUpdate.endedAtUtc)}`,
|
|
239
|
+
`Latest update result: ${snapshot.lastAutoUpdate.outcome}`,
|
|
240
|
+
`Latest update packages updated: ${snapshot.lastAutoUpdate.packagesUpdated}`,
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
if (snapshot.lastAutoUpdate.reason) {
|
|
244
|
+
lines.push(`Latest update detail: ${snapshot.lastAutoUpdate.reason}`)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return lines
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function createStatusReport(
|
|
251
|
+
snapshot: PackageStatusSnapshot,
|
|
252
|
+
): PackageManagerReport {
|
|
253
|
+
return {
|
|
254
|
+
title: PACKAGE_MANAGER_TITLE,
|
|
255
|
+
headline:
|
|
256
|
+
snapshot.availableUpdates.length === 0
|
|
257
|
+
? "No package updates are available."
|
|
258
|
+
: `${snapshot.availableUpdates.length} package update${snapshot.availableUpdates.length === 1 ? " is" : "s are"} available.`,
|
|
259
|
+
tone: snapshot.availableUpdates.length > 0 ? "warning" : "info",
|
|
260
|
+
lines: formatStatusLines(snapshot),
|
|
261
|
+
lineTone: "dim",
|
|
262
|
+
output:
|
|
263
|
+
snapshot.availableUpdates.length > 0
|
|
264
|
+
? snapshot.availableUpdates.map((name) => `- ${name}`).join("\n")
|
|
265
|
+
: undefined,
|
|
266
|
+
outputLabel:
|
|
267
|
+
snapshot.availableUpdates.length > 0 ? "Available updates:" : undefined,
|
|
268
|
+
outputTone: "dim",
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function createAutoUpdateResultReport(input: {
|
|
273
|
+
record: AutoUpdateRecord
|
|
274
|
+
output?: string
|
|
275
|
+
reloadAfterSeconds?: number
|
|
276
|
+
}): PackageManagerReport {
|
|
277
|
+
const lines = [
|
|
278
|
+
`Start: ${formatUtcTimestamp(input.record.startedAtUtc)}`,
|
|
279
|
+
`End: ${formatUtcTimestamp(input.record.endedAtUtc)}`,
|
|
280
|
+
`Result: ${input.record.outcome}`,
|
|
281
|
+
`Packages updated: ${input.record.packagesUpdated}`,
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
if (input.reloadAfterSeconds) {
|
|
285
|
+
lines.push(
|
|
286
|
+
`Reloading in ${input.reloadAfterSeconds} seconds to activate updated package resources.`,
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
title: PACKAGE_MANAGER_TITLE,
|
|
292
|
+
headline: formatAutomaticUpdateHeadline(input.record.outcome),
|
|
293
|
+
tone:
|
|
294
|
+
input.record.outcome === "failed"
|
|
295
|
+
? "error"
|
|
296
|
+
: input.record.outcome === "succeeded"
|
|
297
|
+
? "success"
|
|
298
|
+
: "info",
|
|
299
|
+
lines,
|
|
300
|
+
lineTone: "dim",
|
|
301
|
+
output: input.output ?? input.record.reason,
|
|
302
|
+
outputTone: "dim",
|
|
303
|
+
hideOutputWhenCollapsed: true,
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function createAutomaticUpdateWidgetLines(state: WidgetState): string[] {
|
|
308
|
+
if (state.mode === "status-checking") {
|
|
309
|
+
return ["Pi package status in progress.", "Checking for package updates..."]
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (state.mode === "checking") {
|
|
313
|
+
return [
|
|
314
|
+
formatAutomaticUpdateHeadline("in-progress"),
|
|
315
|
+
"Checking for package updates...",
|
|
316
|
+
]
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (state.mode === "installing") {
|
|
320
|
+
return [
|
|
321
|
+
formatAutomaticUpdateHeadline("in-progress"),
|
|
322
|
+
`Installing ${state.packages} package update${state.packages === 1 ? "" : "s"}...`,
|
|
323
|
+
]
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return [
|
|
327
|
+
formatAutomaticUpdateHeadline("succeeded"),
|
|
328
|
+
`Reloading in ${state.secondsRemaining} second${state.secondsRemaining === 1 ? "" : "s"} to activate updated package resources.`,
|
|
329
|
+
]
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function formatAutomaticUpdateHeadline(
|
|
333
|
+
state: AutoUpdateOutcome | "in-progress",
|
|
334
|
+
): string {
|
|
335
|
+
const suffix =
|
|
336
|
+
state === "in-progress"
|
|
337
|
+
? "in progress"
|
|
338
|
+
: state === "succeeded"
|
|
339
|
+
? "completed"
|
|
340
|
+
: state
|
|
341
|
+
|
|
342
|
+
return `Pi package(s) update ${suffix}.`
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function getLastAutoUpdateRecord(
|
|
346
|
+
entries: readonly SessionEntry[],
|
|
347
|
+
): AutoUpdateRecord | undefined {
|
|
348
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
349
|
+
const entry = entries[index]
|
|
350
|
+
|
|
351
|
+
if (!isAutoUpdateRecordEntry(entry)) {
|
|
352
|
+
continue
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return entry.data
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return undefined
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function handleStatusCommand(
|
|
362
|
+
pi: ExtensionAPI,
|
|
363
|
+
ctx: ExtensionCommandContext,
|
|
364
|
+
lastAutoUpdateRecord: AutoUpdateRecord | undefined,
|
|
365
|
+
): Promise<void> {
|
|
366
|
+
setPackageManagerWidget(ctx, { mode: "status-checking" })
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const availableUpdates = await checkForAvailableUpdates(ctx)
|
|
370
|
+
pi.appendEntry(
|
|
371
|
+
REPORT_ENTRY_TYPE,
|
|
372
|
+
createStatusReport({
|
|
373
|
+
availableUpdates,
|
|
374
|
+
lastAutoUpdate: lastAutoUpdateRecord,
|
|
375
|
+
}),
|
|
376
|
+
)
|
|
377
|
+
} catch (error) {
|
|
378
|
+
pi.appendEntry(REPORT_ENTRY_TYPE, {
|
|
379
|
+
title: PACKAGE_MANAGER_TITLE,
|
|
380
|
+
headline: "Package status check failed.",
|
|
381
|
+
tone: "error",
|
|
382
|
+
lines: formatStatusLines({
|
|
383
|
+
availableUpdates: [],
|
|
384
|
+
lastAutoUpdate: lastAutoUpdateRecord,
|
|
385
|
+
}),
|
|
386
|
+
lineTone: "dim",
|
|
387
|
+
output: `Failed to check package updates: ${getErrorMessage(error)}`,
|
|
388
|
+
outputLabel: "Error detail:",
|
|
389
|
+
outputTone: "dim",
|
|
390
|
+
})
|
|
391
|
+
} finally {
|
|
392
|
+
clearPackageManagerWidget(ctx)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function runUpdate(
|
|
397
|
+
pi: ExtensionAPI,
|
|
398
|
+
ctx: ExtensionCommandContext,
|
|
399
|
+
options: { startupTriggered: boolean },
|
|
400
|
+
): Promise<{ autoUpdateRecord: AutoUpdateRecord }> {
|
|
401
|
+
const startedAtUtc = new Date().toISOString()
|
|
402
|
+
let shouldClearWidget = true
|
|
403
|
+
|
|
404
|
+
setPackageManagerWidget(ctx, { mode: "checking" })
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
if (process.env.PI_OFFLINE) {
|
|
408
|
+
const record = createAutoUpdateRecord({
|
|
409
|
+
startedAtUtc,
|
|
410
|
+
endedAtUtc: new Date().toISOString(),
|
|
411
|
+
outcome: "skipped",
|
|
412
|
+
packagesUpdated: 0,
|
|
413
|
+
reason: "PI_OFFLINE is set.",
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
pi.appendEntry(AUTO_UPDATE_RECORD_ENTRY_TYPE, record)
|
|
417
|
+
pi.appendEntry(REPORT_ENTRY_TYPE, createAutoUpdateResultReport({ record }))
|
|
418
|
+
return { autoUpdateRecord: record }
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const availableUpdates = await checkForAvailableUpdates(ctx)
|
|
422
|
+
|
|
423
|
+
if (availableUpdates.length === 0) {
|
|
424
|
+
const record = createAutoUpdateRecord({
|
|
425
|
+
startedAtUtc,
|
|
426
|
+
endedAtUtc: new Date().toISOString(),
|
|
427
|
+
outcome: "skipped",
|
|
428
|
+
packagesUpdated: 0,
|
|
429
|
+
reason: "No package updates are available.",
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
pi.appendEntry(AUTO_UPDATE_RECORD_ENTRY_TYPE, record)
|
|
433
|
+
pi.appendEntry(REPORT_ENTRY_TYPE, createAutoUpdateResultReport({ record }))
|
|
434
|
+
return { autoUpdateRecord: record }
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
setPackageManagerWidget(ctx, {
|
|
438
|
+
mode: "installing",
|
|
439
|
+
packages: availableUpdates.length,
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
const result = await pi.exec("pi", [...UPDATE_COMMAND], {
|
|
443
|
+
cwd: ctx.cwd,
|
|
444
|
+
signal: ctx.signal,
|
|
445
|
+
})
|
|
446
|
+
const output = getExecDisplayOutput(result)
|
|
447
|
+
|
|
448
|
+
if (result.code === 0) {
|
|
449
|
+
const record = createAutoUpdateRecord({
|
|
450
|
+
startedAtUtc,
|
|
451
|
+
endedAtUtc: new Date().toISOString(),
|
|
452
|
+
outcome: "succeeded",
|
|
453
|
+
packagesUpdated: availableUpdates.length,
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
pi.appendEntry(AUTO_UPDATE_RECORD_ENTRY_TYPE, record)
|
|
457
|
+
pi.appendEntry(
|
|
458
|
+
REPORT_ENTRY_TYPE,
|
|
459
|
+
createAutoUpdateResultReport({
|
|
460
|
+
record,
|
|
461
|
+
output,
|
|
462
|
+
reloadAfterSeconds: RELOAD_COUNTDOWN_SECONDS,
|
|
463
|
+
}),
|
|
464
|
+
)
|
|
465
|
+
await runReloadCountdown(ctx)
|
|
466
|
+
clearPackageManagerWidget(ctx)
|
|
467
|
+
shouldClearWidget = false
|
|
468
|
+
await ctx.reload()
|
|
469
|
+
return { autoUpdateRecord: record }
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const record = createAutoUpdateRecord({
|
|
473
|
+
startedAtUtc,
|
|
474
|
+
endedAtUtc: new Date().toISOString(),
|
|
475
|
+
outcome: "failed",
|
|
476
|
+
packagesUpdated: 0,
|
|
477
|
+
reason: getExecFailureDetail(result),
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
pi.appendEntry(AUTO_UPDATE_RECORD_ENTRY_TYPE, record)
|
|
481
|
+
pi.appendEntry(
|
|
482
|
+
REPORT_ENTRY_TYPE,
|
|
483
|
+
createAutoUpdateResultReport({ record, output }),
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
if (options.startupTriggered && ctx.hasUI) {
|
|
487
|
+
ctx.ui.notify(
|
|
488
|
+
"Pi Package Manager automatic startup update failed. See transcript for details.",
|
|
489
|
+
"error",
|
|
490
|
+
)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return { autoUpdateRecord: record }
|
|
494
|
+
} catch (error) {
|
|
495
|
+
const record = createAutoUpdateRecord({
|
|
496
|
+
startedAtUtc,
|
|
497
|
+
endedAtUtc: new Date().toISOString(),
|
|
498
|
+
outcome: "failed",
|
|
499
|
+
packagesUpdated: 0,
|
|
500
|
+
reason: getErrorMessage(error),
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
pi.appendEntry(AUTO_UPDATE_RECORD_ENTRY_TYPE, record)
|
|
504
|
+
pi.appendEntry(REPORT_ENTRY_TYPE, createAutoUpdateResultReport({ record }))
|
|
505
|
+
|
|
506
|
+
if (options.startupTriggered && ctx.hasUI) {
|
|
507
|
+
ctx.ui.notify(
|
|
508
|
+
"Pi Package Manager automatic startup update failed. See transcript for details.",
|
|
509
|
+
"error",
|
|
510
|
+
)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return { autoUpdateRecord: record }
|
|
514
|
+
} finally {
|
|
515
|
+
if (shouldClearWidget) {
|
|
516
|
+
clearPackageManagerWidget(ctx)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async function runReloadCountdown(
|
|
522
|
+
ctx: ExtensionContext,
|
|
523
|
+
seconds = RELOAD_COUNTDOWN_SECONDS,
|
|
524
|
+
): Promise<void> {
|
|
525
|
+
for (let remaining = seconds; remaining >= 1; remaining--) {
|
|
526
|
+
setPackageManagerWidget(ctx, {
|
|
527
|
+
mode: "countdown",
|
|
528
|
+
secondsRemaining: remaining,
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
await delay(1000)
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function checkForAvailableUpdates(ctx: ExtensionContext): Promise<string[]> {
|
|
536
|
+
const agentDir = getAgentDir()
|
|
537
|
+
const settingsManager = SettingsManager.create(ctx.cwd, agentDir, {
|
|
538
|
+
projectTrusted: ctx.isProjectTrusted(),
|
|
539
|
+
})
|
|
540
|
+
const packageManager = new DefaultPackageManager({
|
|
541
|
+
cwd: ctx.cwd,
|
|
542
|
+
agentDir,
|
|
543
|
+
settingsManager,
|
|
544
|
+
})
|
|
545
|
+
const updates = await packageManager.checkForAvailableUpdates()
|
|
546
|
+
|
|
547
|
+
return updates
|
|
548
|
+
.map((update) => update.displayName)
|
|
549
|
+
.sort((left, right) => left.localeCompare(right))
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function setPackageManagerWidget(ctx: ExtensionContext, state: WidgetState): void {
|
|
553
|
+
if (ctx.mode !== "tui") {
|
|
554
|
+
return
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
ctx.ui.setWidget(
|
|
558
|
+
PACKAGE_MANAGER_WIDGET_KEY,
|
|
559
|
+
(_tui, theme) => {
|
|
560
|
+
const background =
|
|
561
|
+
state.mode === "countdown"
|
|
562
|
+
? (text: string) => theme.bg("toolSuccessBg", text)
|
|
563
|
+
: (text: string) => theme.bg("toolPendingBg", text)
|
|
564
|
+
const box = new Box(1, 1, background)
|
|
565
|
+
const titleColor = state.mode === "countdown" ? "success" : "accent"
|
|
566
|
+
const bodyLines = createAutomaticUpdateWidgetLines(state)
|
|
567
|
+
|
|
568
|
+
box.addChild(
|
|
569
|
+
new Text(
|
|
570
|
+
`${theme.fg(titleColor, "●")} ${theme.bold(theme.fg("customMessageLabel", PACKAGE_MANAGER_TITLE))}`,
|
|
571
|
+
0,
|
|
572
|
+
0,
|
|
573
|
+
),
|
|
574
|
+
)
|
|
575
|
+
box.addChild(
|
|
576
|
+
new Text(
|
|
577
|
+
bodyLines
|
|
578
|
+
.map((line, index) =>
|
|
579
|
+
theme.fg(
|
|
580
|
+
index === bodyLines.length - 1 ? "dim" : "text",
|
|
581
|
+
line,
|
|
582
|
+
),
|
|
583
|
+
)
|
|
584
|
+
.join("\n"),
|
|
585
|
+
0,
|
|
586
|
+
0,
|
|
587
|
+
),
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
return box
|
|
591
|
+
},
|
|
592
|
+
{ placement: "aboveEditor" },
|
|
593
|
+
)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function clearPackageManagerWidget(ctx: ExtensionContext): void {
|
|
597
|
+
if (ctx.mode !== "tui") {
|
|
598
|
+
return
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
ctx.ui.setWidget(PACKAGE_MANAGER_WIDGET_KEY, undefined)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function formatExpandHint(
|
|
605
|
+
theme: { fg(token: string, text: string): string },
|
|
606
|
+
description: string,
|
|
607
|
+
): string {
|
|
608
|
+
const expandKey = keyText("app.tools.expand") || "Ctrl+O"
|
|
609
|
+
|
|
610
|
+
return `${theme.fg("dim", expandKey)}${theme.fg("muted", ` ${description}`)}`
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function formatReportOutput(
|
|
614
|
+
output: string,
|
|
615
|
+
expanded: boolean,
|
|
616
|
+
): { text: string; truncated: boolean } {
|
|
617
|
+
const normalizedLines = output.trim().split(/\r?\n/)
|
|
618
|
+
|
|
619
|
+
if (expanded || normalizedLines.length <= OUTPUT_PREVIEW_LINE_COUNT) {
|
|
620
|
+
return {
|
|
621
|
+
text: normalizedLines.join("\n"),
|
|
622
|
+
truncated: false,
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return {
|
|
627
|
+
text: normalizedLines.slice(0, OUTPUT_PREVIEW_LINE_COUNT).join("\n"),
|
|
628
|
+
truncated: true,
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function getExecDisplayOutput(result: ExecResult): string | undefined {
|
|
633
|
+
const stdout = result.stdout.trim()
|
|
634
|
+
const stderr = result.stderr.trim()
|
|
635
|
+
const sections: string[] = []
|
|
636
|
+
|
|
637
|
+
if (stdout) {
|
|
638
|
+
sections.push(stdout)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (stderr) {
|
|
642
|
+
sections.push(`[stderr]\n${stderr}`)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return sections.length > 0 ? sections.join("\n\n") : undefined
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function isAutoUpdateRecordEntry(
|
|
649
|
+
entry: SessionEntry,
|
|
650
|
+
): entry is CustomEntry<AutoUpdateRecord> {
|
|
651
|
+
return (
|
|
652
|
+
entry.type === "custom" &&
|
|
653
|
+
entry.customType === AUTO_UPDATE_RECORD_ENTRY_TYPE &&
|
|
654
|
+
isAutoUpdateRecord(entry.data)
|
|
655
|
+
)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function isAutoUpdateRecord(value: unknown): value is AutoUpdateRecord {
|
|
659
|
+
if (!value || typeof value !== "object") {
|
|
660
|
+
return false
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const candidate = value as Partial<AutoUpdateRecord>
|
|
664
|
+
return (
|
|
665
|
+
typeof candidate.startedAtUtc === "string" &&
|
|
666
|
+
typeof candidate.endedAtUtc === "string" &&
|
|
667
|
+
(candidate.outcome === "succeeded" ||
|
|
668
|
+
candidate.outcome === "failed" ||
|
|
669
|
+
candidate.outcome === "skipped") &&
|
|
670
|
+
typeof candidate.packagesUpdated === "number" &&
|
|
671
|
+
(candidate.reason === undefined || typeof candidate.reason === "string")
|
|
672
|
+
)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function getExecFailureDetail(result: ExecResult): string {
|
|
676
|
+
const stderr = result.stderr.trim()
|
|
677
|
+
const stdout = result.stdout.trim()
|
|
678
|
+
const detail = stderr || stdout || "Package update command failed."
|
|
679
|
+
|
|
680
|
+
return detail.length > 400 ? `${detail.slice(0, 397)}...` : detail
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function getErrorMessage(error: unknown): string {
|
|
684
|
+
if (error instanceof Error) {
|
|
685
|
+
return error.message
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return String(error)
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function delay(milliseconds: number): Promise<void> {
|
|
692
|
+
return new Promise((resolve) => {
|
|
693
|
+
setTimeout(resolve, milliseconds)
|
|
694
|
+
})
|
|
695
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gaosh3n/pi-package-manager",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Package Manager for Pi",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"packageManager": "pnpm@11.23.0",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi",
|
|
9
|
+
"extension",
|
|
10
|
+
"pi-package",
|
|
11
|
+
"package-manager"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/gaosh3n/pi-extensions.git",
|
|
17
|
+
"directory": "packages/pi-package-manager"
|
|
18
|
+
},
|
|
19
|
+
"pi": {
|
|
20
|
+
"extensions": [
|
|
21
|
+
"./index.ts"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
26
|
+
"@earendil-works/pi-tui": "*"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"index.ts",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22.19.0"
|
|
38
|
+
}
|
|
39
|
+
}
|