@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.
- package/README.md +11 -0
- package/index.ts +76 -655
- package/internal/controller.ts +298 -0
- package/internal/model.ts +45 -0
- package/internal/records.ts +81 -0
- package/internal/reports.ts +396 -0
- package/internal/runtime.ts +69 -0
- package/package.json +2 -1
|
@@ -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
|
+
}
|