@gaosh3n/pi-package-manager 0.1.0 → 0.1.2

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.
@@ -0,0 +1,298 @@
1
+ import {
2
+ type ExtensionAPI,
3
+ type ExtensionCommandContext,
4
+ type ExtensionContext,
5
+ type SessionStartEvent,
6
+ } from "@earendil-works/pi-coding-agent"
7
+
8
+ import { RELOAD_COUNTDOWN_SECONDS, REPORT_ENTRY_TYPE } from "./model.ts"
9
+ import {
10
+ appendAutoUpdateRecordAndReport,
11
+ createAutoUpdateRecord,
12
+ getLastAutoUpdateRecord,
13
+ } from "./records.ts"
14
+ import {
15
+ clearPackageManagerWidget,
16
+ createAutoUpdateResultReport,
17
+ createInstallResultReport,
18
+ createStatusErrorReport,
19
+ createStatusReport,
20
+ getExecDisplayOutput,
21
+ getExecFailureDetail,
22
+ setPackageManagerWidget,
23
+ } from "./reports.ts"
24
+ import { defaultPackageManagerDeps, type PackageManagerDeps } from "./runtime.ts"
25
+
26
+ export function createPackageManagerController(
27
+ pi: Pick<ExtensionAPI, "appendEntry" | "sendUserMessage" | "exec">,
28
+ deps: PackageManagerDeps = defaultPackageManagerDeps,
29
+ ) {
30
+ return {
31
+ onSessionStart,
32
+ handleStatus,
33
+ handleUpdate,
34
+ handleInstall,
35
+ }
36
+
37
+ async function onSessionStart(
38
+ event: Pick<SessionStartEvent, "reason">,
39
+ _ctx: ExtensionContext,
40
+ ): Promise<void> {
41
+ if (!shouldAutoUpdateOnSessionStart(event)) {
42
+ return
43
+ }
44
+
45
+ pi.sendUserMessage("/package-manager update --startup", {
46
+ expandPromptTemplates: true,
47
+ })
48
+ }
49
+
50
+ async function handleStatus(ctx: ExtensionCommandContext): Promise<void> {
51
+ const lastAutoUpdate = getLastAutoUpdateRecord(ctx.sessionManager.getEntries())
52
+
53
+ setPackageManagerWidget(ctx, { mode: "status-checking" })
54
+
55
+ try {
56
+ const availableUpdates = await deps.checkForAvailableUpdates(ctx)
57
+ pi.appendEntry(
58
+ REPORT_ENTRY_TYPE,
59
+ createStatusReport({
60
+ availableUpdates,
61
+ lastAutoUpdate,
62
+ }),
63
+ )
64
+ } catch (error) {
65
+ pi.appendEntry(
66
+ REPORT_ENTRY_TYPE,
67
+ createStatusErrorReport(
68
+ {
69
+ availableUpdates: [],
70
+ lastAutoUpdate,
71
+ },
72
+ getErrorMessage(error),
73
+ ),
74
+ )
75
+ } finally {
76
+ clearPackageManagerWidget(ctx)
77
+ }
78
+ }
79
+
80
+ async function handleUpdate(
81
+ ctx: ExtensionCommandContext,
82
+ options: { startupTriggered: boolean },
83
+ ): Promise<void> {
84
+ const startedAtUtc = deps.nowIso()
85
+ let shouldClearWidget = true
86
+
87
+ setPackageManagerWidget(ctx, { mode: "checking" })
88
+
89
+ try {
90
+ if (deps.isOffline()) {
91
+ appendSkippedResult(startedAtUtc, "PI_OFFLINE is set.")
92
+ return
93
+ }
94
+
95
+ const availableUpdates = await deps.checkForAvailableUpdates(ctx)
96
+
97
+ if (availableUpdates.length === 0) {
98
+ appendSkippedResult(startedAtUtc, "No package updates are available.")
99
+ return
100
+ }
101
+
102
+ setPackageManagerWidget(ctx, {
103
+ mode: "installing",
104
+ packages: availableUpdates.length,
105
+ })
106
+
107
+ const result = await deps.runNativeUpdate(pi, ctx)
108
+ const output = getExecDisplayOutput(result)
109
+
110
+ if (result.code === 0) {
111
+ const record = createAutoUpdateRecord({
112
+ startedAtUtc,
113
+ endedAtUtc: deps.nowIso(),
114
+ outcome: "succeeded",
115
+ packagesUpdated: availableUpdates.length,
116
+ })
117
+
118
+ appendAutoUpdateRecordAndReport(
119
+ pi,
120
+ record,
121
+ createAutoUpdateResultReport({
122
+ record,
123
+ output,
124
+ reloadAfterSeconds: RELOAD_COUNTDOWN_SECONDS,
125
+ }),
126
+ )
127
+ await runReloadCountdown(ctx)
128
+ clearPackageManagerWidget(ctx)
129
+ shouldClearWidget = false
130
+ await ctx.reload()
131
+ return
132
+ }
133
+
134
+ const record = createAutoUpdateRecord({
135
+ startedAtUtc,
136
+ endedAtUtc: deps.nowIso(),
137
+ outcome: "failed",
138
+ packagesUpdated: 0,
139
+ reason: getExecFailureDetail(result),
140
+ })
141
+
142
+ appendAutoUpdateRecordAndReport(
143
+ pi,
144
+ record,
145
+ createAutoUpdateResultReport({ record, output }),
146
+ )
147
+ notifyStartupFailure(ctx, options.startupTriggered)
148
+ } catch (error) {
149
+ const record = createAutoUpdateRecord({
150
+ startedAtUtc,
151
+ endedAtUtc: deps.nowIso(),
152
+ outcome: "failed",
153
+ packagesUpdated: 0,
154
+ reason: getErrorMessage(error),
155
+ })
156
+
157
+ appendAutoUpdateRecordAndReport(
158
+ pi,
159
+ record,
160
+ createAutoUpdateResultReport({ record }),
161
+ )
162
+ notifyStartupFailure(ctx, options.startupTriggered)
163
+ } finally {
164
+ if (shouldClearWidget) {
165
+ clearPackageManagerWidget(ctx)
166
+ }
167
+ }
168
+ }
169
+
170
+ async function handleInstall(ctx: ExtensionCommandContext): Promise<void> {
171
+ if (!ctx.hasUI) {
172
+ ctx.ui.notify(
173
+ "/package-manager install requires dialog-capable UI.",
174
+ "warning",
175
+ )
176
+ return
177
+ }
178
+
179
+ const source = (
180
+ await ctx.ui.input(
181
+ "Install Pi Package",
182
+ "npm:@scope/pkg or git:github.com/user/repo",
183
+ )
184
+ )?.trim()
185
+
186
+ if (source === undefined) {
187
+ return
188
+ }
189
+
190
+ if (!source) {
191
+ ctx.ui.notify("Package source is required.", "warning")
192
+ return
193
+ }
194
+
195
+ const startedAtUtc = deps.nowIso()
196
+ setPackageManagerWidget(ctx, { mode: "package-installing", source })
197
+
198
+ try {
199
+ const result = await deps.runNativeInstall(pi, ctx, source)
200
+ const output = getExecDisplayOutput(result)
201
+
202
+ if (result.code === 0) {
203
+ pi.appendEntry(
204
+ REPORT_ENTRY_TYPE,
205
+ createInstallResultReport({
206
+ startedAtUtc,
207
+ endedAtUtc: deps.nowIso(),
208
+ source,
209
+ outcome: "succeeded",
210
+ output,
211
+ }),
212
+ )
213
+ return
214
+ }
215
+
216
+ pi.appendEntry(
217
+ REPORT_ENTRY_TYPE,
218
+ createInstallResultReport({
219
+ startedAtUtc,
220
+ endedAtUtc: deps.nowIso(),
221
+ source,
222
+ outcome: "failed",
223
+ output,
224
+ reason: getExecFailureDetail(result),
225
+ }),
226
+ )
227
+ } catch (error) {
228
+ pi.appendEntry(
229
+ REPORT_ENTRY_TYPE,
230
+ createInstallResultReport({
231
+ startedAtUtc,
232
+ endedAtUtc: deps.nowIso(),
233
+ source,
234
+ outcome: "failed",
235
+ reason: getErrorMessage(error),
236
+ }),
237
+ )
238
+ } finally {
239
+ clearPackageManagerWidget(ctx)
240
+ }
241
+ }
242
+
243
+ function appendSkippedResult(startedAtUtc: string, reason: string): void {
244
+ const record = createAutoUpdateRecord({
245
+ startedAtUtc,
246
+ endedAtUtc: deps.nowIso(),
247
+ outcome: "skipped",
248
+ packagesUpdated: 0,
249
+ reason,
250
+ })
251
+
252
+ appendAutoUpdateRecordAndReport(
253
+ pi,
254
+ record,
255
+ createAutoUpdateResultReport({ record }),
256
+ )
257
+ }
258
+
259
+ async function runReloadCountdown(
260
+ ctx: ExtensionContext,
261
+ seconds = RELOAD_COUNTDOWN_SECONDS,
262
+ ): Promise<void> {
263
+ for (let remaining = seconds; remaining >= 1; remaining--) {
264
+ setPackageManagerWidget(ctx, {
265
+ mode: "countdown",
266
+ secondsRemaining: remaining,
267
+ })
268
+
269
+ await deps.sleep(1000)
270
+ }
271
+ }
272
+ }
273
+
274
+ export function shouldAutoUpdateOnSessionStart(
275
+ event: Pick<SessionStartEvent, "reason">,
276
+ ): boolean {
277
+ return event.reason === "startup"
278
+ }
279
+
280
+ function notifyStartupFailure(
281
+ ctx: ExtensionCommandContext,
282
+ startupTriggered: boolean,
283
+ ): void {
284
+ if (startupTriggered && ctx.hasUI) {
285
+ ctx.ui.notify(
286
+ "Pi Package Manager automatic startup update failed. See transcript for details.",
287
+ "error",
288
+ )
289
+ }
290
+ }
291
+
292
+ function getErrorMessage(error: unknown): string {
293
+ if (error instanceof Error) {
294
+ return error.message
295
+ }
296
+
297
+ return String(error)
298
+ }
@@ -0,0 +1,45 @@
1
+ export const AUTO_UPDATE_RECORD_ENTRY_TYPE = "package-manager-auto-update-record"
2
+ export const REPORT_ENTRY_TYPE = "package-manager-report"
3
+ export const PACKAGE_MANAGER_TITLE = "Pi Package Manager"
4
+ export const PACKAGE_MANAGER_WIDGET_KEY = "pi-package-manager"
5
+ export const RELOAD_COUNTDOWN_SECONDS = 5
6
+ export const OUTPUT_PREVIEW_LINE_COUNT = 8
7
+ export const UPDATE_COMMAND = ["update", "--extensions"] as const
8
+ export const INSTALL_COMMAND = ["install"] as const
9
+
10
+ export type AutoUpdateOutcome = "succeeded" | "failed" | "skipped"
11
+ export type InstallOutcome = "succeeded" | "failed"
12
+ export type ReportTone = "info" | "success" | "warning" | "error"
13
+
14
+ export type WidgetState =
15
+ | { mode: "status-checking" }
16
+ | { mode: "checking" }
17
+ | { mode: "installing"; packages: number }
18
+ | { mode: "countdown"; secondsRemaining: number }
19
+ | { mode: "package-installing"; source: string }
20
+
21
+ export interface AutoUpdateRecord {
22
+ startedAtUtc: string
23
+ endedAtUtc: string
24
+ outcome: AutoUpdateOutcome
25
+ packagesUpdated: number
26
+ reason?: string
27
+ }
28
+
29
+ export interface PackageManagerReport {
30
+ title: string
31
+ headline?: string
32
+ tone: ReportTone
33
+ lines: string[]
34
+ lineTone?: "default" | "dim"
35
+ output?: string
36
+ outputLabel?: string
37
+ outputDescription?: string
38
+ outputTone?: "default" | "dim"
39
+ hideOutputWhenCollapsed?: boolean
40
+ }
41
+
42
+ export interface PackageStatusSnapshot {
43
+ availableUpdates: string[]
44
+ lastAutoUpdate?: AutoUpdateRecord
45
+ }
@@ -0,0 +1,81 @@
1
+ import {
2
+ type CustomEntry,
3
+ type ExtensionAPI,
4
+ type SessionEntry,
5
+ } from "@earendil-works/pi-coding-agent"
6
+
7
+ import {
8
+ AUTO_UPDATE_RECORD_ENTRY_TYPE,
9
+ REPORT_ENTRY_TYPE,
10
+ type AutoUpdateOutcome,
11
+ type AutoUpdateRecord,
12
+ type PackageManagerReport,
13
+ } from "./model.ts"
14
+
15
+ export function createAutoUpdateRecord(input: {
16
+ startedAtUtc: string
17
+ endedAtUtc: string
18
+ outcome: AutoUpdateOutcome
19
+ packagesUpdated: number
20
+ reason?: string
21
+ }): AutoUpdateRecord {
22
+ return {
23
+ startedAtUtc: input.startedAtUtc,
24
+ endedAtUtc: input.endedAtUtc,
25
+ outcome: input.outcome,
26
+ packagesUpdated: input.packagesUpdated,
27
+ reason: input.reason,
28
+ }
29
+ }
30
+
31
+ export function appendAutoUpdateRecordAndReport(
32
+ pi: Pick<ExtensionAPI, "appendEntry">,
33
+ record: AutoUpdateRecord,
34
+ report: PackageManagerReport,
35
+ ): void {
36
+ pi.appendEntry(AUTO_UPDATE_RECORD_ENTRY_TYPE, record)
37
+ pi.appendEntry(REPORT_ENTRY_TYPE, report)
38
+ }
39
+
40
+ export function getLastAutoUpdateRecord(
41
+ entries: readonly SessionEntry[],
42
+ ): AutoUpdateRecord | undefined {
43
+ for (let index = entries.length - 1; index >= 0; index--) {
44
+ const entry = entries[index]
45
+
46
+ if (!isAutoUpdateRecordEntry(entry)) {
47
+ continue
48
+ }
49
+
50
+ return entry.data
51
+ }
52
+
53
+ return undefined
54
+ }
55
+
56
+ export function isAutoUpdateRecordEntry(
57
+ entry: SessionEntry,
58
+ ): entry is CustomEntry<AutoUpdateRecord> {
59
+ return (
60
+ entry.type === "custom" &&
61
+ entry.customType === AUTO_UPDATE_RECORD_ENTRY_TYPE &&
62
+ isAutoUpdateRecord(entry.data)
63
+ )
64
+ }
65
+
66
+ export function isAutoUpdateRecord(value: unknown): value is AutoUpdateRecord {
67
+ if (!value || typeof value !== "object") {
68
+ return false
69
+ }
70
+
71
+ const candidate = value as Partial<AutoUpdateRecord>
72
+ return (
73
+ typeof candidate.startedAtUtc === "string" &&
74
+ typeof candidate.endedAtUtc === "string" &&
75
+ (candidate.outcome === "succeeded" ||
76
+ candidate.outcome === "failed" ||
77
+ candidate.outcome === "skipped") &&
78
+ typeof candidate.packagesUpdated === "number" &&
79
+ (candidate.reason === undefined || typeof candidate.reason === "string")
80
+ )
81
+ }