@gaosh3n/pi-package-manager 0.1.0 → 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/index.ts CHANGED
@@ -1,169 +1,52 @@
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
- }
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
2
+ import { type AutocompleteItem } from "@earendil-works/pi-tui"
53
3
 
54
- export interface PackageStatusSnapshot {
55
- availableUpdates: string[]
56
- lastAutoUpdate?: AutoUpdateRecord
57
- }
4
+ import {
5
+ createPackageManagerController,
6
+ shouldAutoUpdateOnSessionStart,
7
+ } from "./internal/controller.ts"
8
+ import {
9
+ createAutoUpdateRecord,
10
+ getLastAutoUpdateRecord,
11
+ isAutoUpdateRecord,
12
+ isAutoUpdateRecordEntry,
13
+ } from "./internal/records.ts"
14
+ import {
15
+ AUTO_UPDATE_RECORD_ENTRY_TYPE,
16
+ PACKAGE_MANAGER_TITLE,
17
+ REPORT_ENTRY_TYPE,
18
+ type AutoUpdateOutcome,
19
+ type AutoUpdateRecord,
20
+ type PackageManagerReport,
21
+ type PackageStatusSnapshot,
22
+ type ReportTone,
23
+ type WidgetState,
24
+ } from "./internal/model.ts"
25
+ import {
26
+ createAutomaticUpdateWidgetLines,
27
+ createAutoUpdateResultReport,
28
+ createReportEntryRenderer,
29
+ createStatusReport,
30
+ formatStatusLines,
31
+ formatUtcTimestamp,
32
+ } from "./internal/reports.ts"
33
+ import {
34
+ defaultPackageManagerDeps,
35
+ type PackageManagerDeps,
36
+ } from "./internal/runtime.ts"
58
37
 
59
- export default function (pi: ExtensionAPI) {
60
- let lastAutoUpdateRecord: AutoUpdateRecord | undefined
38
+ export default function initPackageManager(
39
+ pi: ExtensionAPI,
40
+ deps: PackageManagerDeps = defaultPackageManagerDeps,
41
+ ): void {
42
+ const controller = createPackageManagerController(pi, deps)
61
43
 
62
44
  pi.registerEntryRenderer<PackageManagerReport>(
63
45
  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
- },
46
+ createReportEntryRenderer(),
154
47
  )
155
48
 
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
- })
49
+ pi.on("session_start", controller.onSessionStart)
167
50
 
168
51
  pi.registerCommand("package-manager", {
169
52
  description:
@@ -184,16 +67,14 @@ export default function (pi: ExtensionAPI) {
184
67
  const subcommand = tokens[0] ?? "status"
185
68
 
186
69
  if (subcommand === "status") {
187
- await handleStatusCommand(pi, ctx, lastAutoUpdateRecord)
70
+ await controller.handleStatus(ctx)
188
71
  return
189
72
  }
190
73
 
191
74
  if (subcommand === "update") {
192
- const result = await runUpdate(pi, ctx, {
75
+ await controller.handleUpdate(ctx, {
193
76
  startupTriggered: tokens.includes("--startup"),
194
77
  })
195
-
196
- lastAutoUpdateRecord = result.autoUpdateRecord
197
78
  return
198
79
  }
199
80
 
@@ -202,494 +83,27 @@ export default function (pi: ExtensionAPI) {
202
83
  })
203
84
  }
204
85
 
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
- })
86
+ export {
87
+ AUTO_UPDATE_RECORD_ENTRY_TYPE,
88
+ PACKAGE_MANAGER_TITLE,
89
+ REPORT_ENTRY_TYPE,
90
+ createAutomaticUpdateWidgetLines,
91
+ createAutoUpdateRecord,
92
+ createAutoUpdateResultReport,
93
+ createStatusReport,
94
+ formatStatusLines,
95
+ formatUtcTimestamp,
96
+ getLastAutoUpdateRecord,
97
+ isAutoUpdateRecord,
98
+ isAutoUpdateRecordEntry,
99
+ shouldAutoUpdateOnSessionStart,
100
+ }
101
+ export type {
102
+ AutoUpdateOutcome,
103
+ AutoUpdateRecord,
104
+ PackageManagerDeps,
105
+ PackageManagerReport,
106
+ PackageStatusSnapshot,
107
+ ReportTone,
108
+ WidgetState,
695
109
  }