@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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/index.ts +695 -0
  4. 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
+ }