@gaosh3n/pi-package-manager 0.1.1 → 0.1.3
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 +22 -0
- package/index.ts +20 -3
- package/internal/controller.ts +200 -1
- package/internal/model.ts +18 -0
- package/internal/reports.ts +111 -8
- package/internal/runtime.ts +63 -10
- package/internal/uninstall-picker.ts +144 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
- a visible progress UI while Pi checks or updates packages
|
|
9
9
|
- `/package-manager status` to see whether updates are available
|
|
10
10
|
- `/package-manager update` to run the update flow on demand
|
|
11
|
+
- `/package-manager install` to install a Pi package from an entered source
|
|
12
|
+
- `/package-manager uninstall` to remove one or more installed Pi packages from a checkbox-style picker
|
|
11
13
|
- a final result card in Pi so you can review the latest outcome
|
|
12
14
|
- automatic Pi reload after a successful startup update
|
|
13
15
|
|
|
@@ -44,3 +46,23 @@ Run:
|
|
|
44
46
|
```
|
|
45
47
|
|
|
46
48
|
Pi will run the update flow for you, show live progress, and record the final result in the transcript.
|
|
49
|
+
|
|
50
|
+
### Install a package
|
|
51
|
+
|
|
52
|
+
Run:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
/package-manager install
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Pi will prompt you for a package source such as `npm:@foo/bar` or `git:github.com/user/repo`, run the native `pi install ...` flow, and show a final result card. After a successful install, run `/reload` to activate the installed package resources.
|
|
59
|
+
|
|
60
|
+
### Uninstall package(s)
|
|
61
|
+
|
|
62
|
+
Run:
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
/package-manager uninstall
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Pi will show a checkbox-style package picker. Use <space> to toggle package selection, then press Enter to confirm. Pi runs native `pi uninstall <source>` once per selected package and shows one final result card. After successful or partial removal, run `/reload` to deactivate removed package resources.
|
package/index.ts
CHANGED
|
@@ -25,7 +25,9 @@ import {
|
|
|
25
25
|
import {
|
|
26
26
|
createAutomaticUpdateWidgetLines,
|
|
27
27
|
createAutoUpdateResultReport,
|
|
28
|
+
createInstallResultReport,
|
|
28
29
|
createReportEntryRenderer,
|
|
30
|
+
createUninstallResultReport,
|
|
29
31
|
createStatusReport,
|
|
30
32
|
formatStatusLines,
|
|
31
33
|
formatUtcTimestamp,
|
|
@@ -50,9 +52,9 @@ export default function initPackageManager(
|
|
|
50
52
|
|
|
51
53
|
pi.registerCommand("package-manager", {
|
|
52
54
|
description:
|
|
53
|
-
"Manage Pi
|
|
55
|
+
"Manage Pi packages (usage: /package-manager [status|update|install|uninstall])",
|
|
54
56
|
getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
|
|
55
|
-
const items = ["status", "update"].map((value) => ({
|
|
57
|
+
const items = ["status", "update", "install", "uninstall"].map((value) => ({
|
|
56
58
|
value,
|
|
57
59
|
label: value,
|
|
58
60
|
}))
|
|
@@ -78,7 +80,20 @@ export default function initPackageManager(
|
|
|
78
80
|
return
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
if (subcommand === "install") {
|
|
84
|
+
await controller.handleInstall(ctx)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (subcommand === "uninstall") {
|
|
89
|
+
await controller.handleUninstall(ctx)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
ctx.ui.notify(
|
|
94
|
+
"Usage: /package-manager [status|update|install|uninstall]",
|
|
95
|
+
"warning",
|
|
96
|
+
)
|
|
82
97
|
},
|
|
83
98
|
})
|
|
84
99
|
}
|
|
@@ -90,6 +105,8 @@ export {
|
|
|
90
105
|
createAutomaticUpdateWidgetLines,
|
|
91
106
|
createAutoUpdateRecord,
|
|
92
107
|
createAutoUpdateResultReport,
|
|
108
|
+
createInstallResultReport,
|
|
109
|
+
createUninstallResultReport,
|
|
93
110
|
createStatusReport,
|
|
94
111
|
formatStatusLines,
|
|
95
112
|
formatUtcTimestamp,
|
package/internal/controller.ts
CHANGED
|
@@ -14,13 +14,16 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
clearPackageManagerWidget,
|
|
16
16
|
createAutoUpdateResultReport,
|
|
17
|
+
createInstallResultReport,
|
|
17
18
|
createStatusErrorReport,
|
|
18
19
|
createStatusReport,
|
|
20
|
+
createUninstallResultReport,
|
|
19
21
|
getExecDisplayOutput,
|
|
20
22
|
getExecFailureDetail,
|
|
21
23
|
setPackageManagerWidget,
|
|
22
24
|
} from "./reports.ts"
|
|
23
25
|
import { defaultPackageManagerDeps, type PackageManagerDeps } from "./runtime.ts"
|
|
26
|
+
import { promptForPackagesToUninstall } from "./uninstall-picker.ts"
|
|
24
27
|
|
|
25
28
|
export function createPackageManagerController(
|
|
26
29
|
pi: Pick<ExtensionAPI, "appendEntry" | "sendUserMessage" | "exec">,
|
|
@@ -30,6 +33,8 @@ export function createPackageManagerController(
|
|
|
30
33
|
onSessionStart,
|
|
31
34
|
handleStatus,
|
|
32
35
|
handleUpdate,
|
|
36
|
+
handleInstall,
|
|
37
|
+
handleUninstall,
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
async function onSessionStart(
|
|
@@ -134,7 +139,7 @@ export function createPackageManagerController(
|
|
|
134
139
|
endedAtUtc: deps.nowIso(),
|
|
135
140
|
outcome: "failed",
|
|
136
141
|
packagesUpdated: 0,
|
|
137
|
-
reason: getExecFailureDetail(result),
|
|
142
|
+
reason: getExecFailureDetail(result, "Package update command failed."),
|
|
138
143
|
})
|
|
139
144
|
|
|
140
145
|
appendAutoUpdateRecordAndReport(
|
|
@@ -165,6 +170,200 @@ export function createPackageManagerController(
|
|
|
165
170
|
}
|
|
166
171
|
}
|
|
167
172
|
|
|
173
|
+
async function handleInstall(ctx: ExtensionCommandContext): Promise<void> {
|
|
174
|
+
if (!ctx.hasUI) {
|
|
175
|
+
ctx.ui.notify(
|
|
176
|
+
"/package-manager install requires dialog-capable UI.",
|
|
177
|
+
"warning",
|
|
178
|
+
)
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const source = (
|
|
183
|
+
await ctx.ui.input(
|
|
184
|
+
"Install Pi Package",
|
|
185
|
+
"npm:@scope/pkg or git:github.com/user/repo",
|
|
186
|
+
)
|
|
187
|
+
)?.trim()
|
|
188
|
+
|
|
189
|
+
if (source === undefined) {
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!source) {
|
|
194
|
+
ctx.ui.notify("Package source is required.", "warning")
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const startedAtUtc = deps.nowIso()
|
|
199
|
+
setPackageManagerWidget(ctx, { mode: "package-installing", source })
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const result = await deps.runNativeInstall(pi, ctx, source)
|
|
203
|
+
const output = getExecDisplayOutput(result)
|
|
204
|
+
|
|
205
|
+
if (result.code === 0) {
|
|
206
|
+
pi.appendEntry(
|
|
207
|
+
REPORT_ENTRY_TYPE,
|
|
208
|
+
createInstallResultReport({
|
|
209
|
+
startedAtUtc,
|
|
210
|
+
endedAtUtc: deps.nowIso(),
|
|
211
|
+
source,
|
|
212
|
+
outcome: "succeeded",
|
|
213
|
+
output,
|
|
214
|
+
}),
|
|
215
|
+
)
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
pi.appendEntry(
|
|
220
|
+
REPORT_ENTRY_TYPE,
|
|
221
|
+
createInstallResultReport({
|
|
222
|
+
startedAtUtc,
|
|
223
|
+
endedAtUtc: deps.nowIso(),
|
|
224
|
+
source,
|
|
225
|
+
outcome: "failed",
|
|
226
|
+
output,
|
|
227
|
+
reason: getExecFailureDetail(
|
|
228
|
+
result,
|
|
229
|
+
"Package install command failed.",
|
|
230
|
+
),
|
|
231
|
+
}),
|
|
232
|
+
)
|
|
233
|
+
} catch (error) {
|
|
234
|
+
pi.appendEntry(
|
|
235
|
+
REPORT_ENTRY_TYPE,
|
|
236
|
+
createInstallResultReport({
|
|
237
|
+
startedAtUtc,
|
|
238
|
+
endedAtUtc: deps.nowIso(),
|
|
239
|
+
source,
|
|
240
|
+
outcome: "failed",
|
|
241
|
+
reason: getErrorMessage(error),
|
|
242
|
+
}),
|
|
243
|
+
)
|
|
244
|
+
} finally {
|
|
245
|
+
clearPackageManagerWidget(ctx)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function handleUninstall(ctx: ExtensionCommandContext): Promise<void> {
|
|
250
|
+
if (ctx.mode !== "tui") {
|
|
251
|
+
ctx.ui.notify("/package-manager uninstall requires TUI mode.", "warning")
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const packages = await deps.listConfiguredPackages(ctx)
|
|
256
|
+
|
|
257
|
+
if (packages.length === 0) {
|
|
258
|
+
ctx.ui.notify("No Pi packages are available to uninstall.", "info")
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const selectedSources = await promptForPackagesToUninstall(ctx, packages)
|
|
263
|
+
|
|
264
|
+
if (selectedSources === undefined) {
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (selectedSources.length === 0) {
|
|
269
|
+
ctx.ui.notify("Select at least one package to uninstall.", "warning")
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const selectedSourceSet = new Set(selectedSources)
|
|
274
|
+
const sources = packages
|
|
275
|
+
.map((pkg) => pkg.source)
|
|
276
|
+
.filter((source) => selectedSourceSet.has(source))
|
|
277
|
+
|
|
278
|
+
if (sources.length === 0) {
|
|
279
|
+
ctx.ui.notify("Select at least one package to uninstall.", "warning")
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const startedAtUtc = deps.nowIso()
|
|
284
|
+
const succeededSources: string[] = []
|
|
285
|
+
const failedSources: string[] = []
|
|
286
|
+
const outputSections: string[] = []
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
for (const [index, source] of sources.entries()) {
|
|
290
|
+
setPackageManagerWidget(ctx, {
|
|
291
|
+
mode: "package-uninstalling",
|
|
292
|
+
current: index + 1,
|
|
293
|
+
total: sources.length,
|
|
294
|
+
source,
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
const result = await deps.runNativeUninstall(pi, ctx, source)
|
|
298
|
+
const output = getExecDisplayOutput(result)
|
|
299
|
+
|
|
300
|
+
if (result.code === 0) {
|
|
301
|
+
if (output) {
|
|
302
|
+
outputSections.push(`[${source}]\n${output}`)
|
|
303
|
+
}
|
|
304
|
+
succeededSources.push(source)
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
failedSources.push(source)
|
|
309
|
+
outputSections.push(
|
|
310
|
+
`[${source}]\n${output ?? getExecFailureDetail(result, "Package uninstall command failed.")}`,
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
pi.appendEntry(
|
|
315
|
+
REPORT_ENTRY_TYPE,
|
|
316
|
+
createUninstallResultReport({
|
|
317
|
+
startedAtUtc,
|
|
318
|
+
endedAtUtc: deps.nowIso(),
|
|
319
|
+
sources,
|
|
320
|
+
outcome:
|
|
321
|
+
failedSources.length === 0
|
|
322
|
+
? "succeeded"
|
|
323
|
+
: succeededSources.length > 0
|
|
324
|
+
? "partial"
|
|
325
|
+
: "failed",
|
|
326
|
+
succeededSources,
|
|
327
|
+
failedSources,
|
|
328
|
+
output:
|
|
329
|
+
outputSections.length > 0
|
|
330
|
+
? outputSections.join("\n\n")
|
|
331
|
+
: undefined,
|
|
332
|
+
reason:
|
|
333
|
+
failedSources.length > 0
|
|
334
|
+
? `Failed to uninstall ${failedSources[0]}.`
|
|
335
|
+
: undefined,
|
|
336
|
+
}),
|
|
337
|
+
)
|
|
338
|
+
} catch (error) {
|
|
339
|
+
const failedSource = sources[succeededSources.length]
|
|
340
|
+
const errorMessage = getErrorMessage(error)
|
|
341
|
+
|
|
342
|
+
if (failedSource && !failedSources.includes(failedSource)) {
|
|
343
|
+
failedSources.push(failedSource)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
outputSections.push(
|
|
347
|
+
failedSource ? `[${failedSource}]\n${errorMessage}` : errorMessage,
|
|
348
|
+
)
|
|
349
|
+
pi.appendEntry(
|
|
350
|
+
REPORT_ENTRY_TYPE,
|
|
351
|
+
createUninstallResultReport({
|
|
352
|
+
startedAtUtc,
|
|
353
|
+
endedAtUtc: deps.nowIso(),
|
|
354
|
+
sources,
|
|
355
|
+
outcome: succeededSources.length > 0 ? "partial" : "failed",
|
|
356
|
+
succeededSources,
|
|
357
|
+
failedSources,
|
|
358
|
+
output: outputSections.join("\n\n"),
|
|
359
|
+
reason: errorMessage,
|
|
360
|
+
}),
|
|
361
|
+
)
|
|
362
|
+
} finally {
|
|
363
|
+
clearPackageManagerWidget(ctx)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
168
367
|
function appendSkippedResult(startedAtUtc: string, reason: string): void {
|
|
169
368
|
const record = createAutoUpdateRecord({
|
|
170
369
|
startedAtUtc,
|
package/internal/model.ts
CHANGED
|
@@ -5,8 +5,12 @@ export const PACKAGE_MANAGER_WIDGET_KEY = "pi-package-manager"
|
|
|
5
5
|
export const RELOAD_COUNTDOWN_SECONDS = 5
|
|
6
6
|
export const OUTPUT_PREVIEW_LINE_COUNT = 8
|
|
7
7
|
export const UPDATE_COMMAND = ["update", "--extensions"] as const
|
|
8
|
+
export const INSTALL_COMMAND = ["install"] as const
|
|
9
|
+
export const UNINSTALL_COMMAND = ["uninstall"] as const
|
|
8
10
|
|
|
9
11
|
export type AutoUpdateOutcome = "succeeded" | "failed" | "skipped"
|
|
12
|
+
export type InstallOutcome = "succeeded" | "failed"
|
|
13
|
+
export type UninstallOutcome = "succeeded" | "partial" | "failed"
|
|
10
14
|
export type ReportTone = "info" | "success" | "warning" | "error"
|
|
11
15
|
|
|
12
16
|
export type WidgetState =
|
|
@@ -14,6 +18,13 @@ export type WidgetState =
|
|
|
14
18
|
| { mode: "checking" }
|
|
15
19
|
| { mode: "installing"; packages: number }
|
|
16
20
|
| { mode: "countdown"; secondsRemaining: number }
|
|
21
|
+
| { mode: "package-installing"; source: string }
|
|
22
|
+
| {
|
|
23
|
+
mode: "package-uninstalling"
|
|
24
|
+
current: number
|
|
25
|
+
total: number
|
|
26
|
+
source: string
|
|
27
|
+
}
|
|
17
28
|
|
|
18
29
|
export interface AutoUpdateRecord {
|
|
19
30
|
startedAtUtc: string
|
|
@@ -31,6 +42,7 @@ export interface PackageManagerReport {
|
|
|
31
42
|
lineTone?: "default" | "dim"
|
|
32
43
|
output?: string
|
|
33
44
|
outputLabel?: string
|
|
45
|
+
outputDescription?: string
|
|
34
46
|
outputTone?: "default" | "dim"
|
|
35
47
|
hideOutputWhenCollapsed?: boolean
|
|
36
48
|
}
|
|
@@ -39,3 +51,9 @@ export interface PackageStatusSnapshot {
|
|
|
39
51
|
availableUpdates: string[]
|
|
40
52
|
lastAutoUpdate?: AutoUpdateRecord
|
|
41
53
|
}
|
|
54
|
+
|
|
55
|
+
export interface ConfiguredPackageOption {
|
|
56
|
+
source: string
|
|
57
|
+
scope: "user" | "project"
|
|
58
|
+
filtered: boolean
|
|
59
|
+
}
|
package/internal/reports.ts
CHANGED
|
@@ -11,8 +11,10 @@ import {
|
|
|
11
11
|
PACKAGE_MANAGER_WIDGET_KEY,
|
|
12
12
|
type AutoUpdateOutcome,
|
|
13
13
|
type AutoUpdateRecord,
|
|
14
|
+
type InstallOutcome,
|
|
14
15
|
type PackageManagerReport,
|
|
15
16
|
type PackageStatusSnapshot,
|
|
17
|
+
type UninstallOutcome,
|
|
16
18
|
type WidgetState,
|
|
17
19
|
} from "./model.ts"
|
|
18
20
|
|
|
@@ -73,7 +75,10 @@ export function createReportEntryRenderer() {
|
|
|
73
75
|
if (!expanded && report.hideOutputWhenCollapsed) {
|
|
74
76
|
box.addChild(
|
|
75
77
|
new Text(
|
|
76
|
-
formatExpandHint(
|
|
78
|
+
formatExpandHint(
|
|
79
|
+
theme,
|
|
80
|
+
`to expand to see ${report.outputDescription ?? "output"}.`,
|
|
81
|
+
),
|
|
77
82
|
0,
|
|
78
83
|
0,
|
|
79
84
|
),
|
|
@@ -83,10 +88,7 @@ export function createReportEntryRenderer() {
|
|
|
83
88
|
|
|
84
89
|
box.addChild(
|
|
85
90
|
new Text(
|
|
86
|
-
theme.fg(
|
|
87
|
-
outputTextTone,
|
|
88
|
-
report.outputLabel ?? "Update output:",
|
|
89
|
-
),
|
|
91
|
+
theme.fg(outputTextTone, report.outputLabel ?? "Output:"),
|
|
90
92
|
0,
|
|
91
93
|
0,
|
|
92
94
|
),
|
|
@@ -98,7 +100,7 @@ export function createReportEntryRenderer() {
|
|
|
98
100
|
new Text(
|
|
99
101
|
formatExpandHint(
|
|
100
102
|
theme,
|
|
101
|
-
|
|
103
|
+
`to expand to view the full ${report.outputDescription ?? "output"}.`,
|
|
102
104
|
),
|
|
103
105
|
0,
|
|
104
106
|
0,
|
|
@@ -203,6 +205,90 @@ export function createAutoUpdateResultReport(input: {
|
|
|
203
205
|
lines,
|
|
204
206
|
lineTone: "dim",
|
|
205
207
|
output: input.output ?? input.record.reason,
|
|
208
|
+
outputLabel: "Update output:",
|
|
209
|
+
outputDescription: "update output",
|
|
210
|
+
outputTone: "dim",
|
|
211
|
+
hideOutputWhenCollapsed: true,
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function createInstallResultReport(input: {
|
|
216
|
+
startedAtUtc: string
|
|
217
|
+
endedAtUtc: string
|
|
218
|
+
source: string
|
|
219
|
+
outcome: InstallOutcome
|
|
220
|
+
output?: string
|
|
221
|
+
reason?: string
|
|
222
|
+
}): PackageManagerReport {
|
|
223
|
+
return {
|
|
224
|
+
title: PACKAGE_MANAGER_TITLE,
|
|
225
|
+
headline:
|
|
226
|
+
input.outcome === "succeeded"
|
|
227
|
+
? "Pi package install completed."
|
|
228
|
+
: "Pi package install failed.",
|
|
229
|
+
tone: input.outcome === "succeeded" ? "success" : "error",
|
|
230
|
+
lines: [
|
|
231
|
+
`Start: ${formatUtcTimestamp(input.startedAtUtc)}`,
|
|
232
|
+
`End: ${formatUtcTimestamp(input.endedAtUtc)}`,
|
|
233
|
+
`Result: ${input.outcome}`,
|
|
234
|
+
`Package source: ${input.source}`,
|
|
235
|
+
...(input.outcome === "succeeded"
|
|
236
|
+
? ["Run /reload to activate installed package resources."]
|
|
237
|
+
: []),
|
|
238
|
+
],
|
|
239
|
+
lineTone: "dim",
|
|
240
|
+
output: input.output ?? input.reason,
|
|
241
|
+
outputLabel:
|
|
242
|
+
input.outcome === "succeeded" ? "Install output:" : "Error detail:",
|
|
243
|
+
outputDescription:
|
|
244
|
+
input.outcome === "succeeded" ? "install output" : "error detail",
|
|
245
|
+
outputTone: "dim",
|
|
246
|
+
hideOutputWhenCollapsed: true,
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function createUninstallResultReport(input: {
|
|
251
|
+
startedAtUtc: string
|
|
252
|
+
endedAtUtc: string
|
|
253
|
+
sources: string[]
|
|
254
|
+
outcome: UninstallOutcome
|
|
255
|
+
succeededSources: string[]
|
|
256
|
+
failedSources: string[]
|
|
257
|
+
output?: string
|
|
258
|
+
reason?: string
|
|
259
|
+
}): PackageManagerReport {
|
|
260
|
+
return {
|
|
261
|
+
title: PACKAGE_MANAGER_TITLE,
|
|
262
|
+
headline:
|
|
263
|
+
input.outcome === "succeeded"
|
|
264
|
+
? "Pi package uninstall completed."
|
|
265
|
+
: input.outcome === "partial"
|
|
266
|
+
? "Pi package uninstall partially completed."
|
|
267
|
+
: "Pi package uninstall failed.",
|
|
268
|
+
tone:
|
|
269
|
+
input.outcome === "succeeded"
|
|
270
|
+
? "success"
|
|
271
|
+
: input.outcome === "partial"
|
|
272
|
+
? "warning"
|
|
273
|
+
: "error",
|
|
274
|
+
lines: [
|
|
275
|
+
`Start: ${formatUtcTimestamp(input.startedAtUtc)}`,
|
|
276
|
+
`End: ${formatUtcTimestamp(input.endedAtUtc)}`,
|
|
277
|
+
`Result: ${input.outcome}`,
|
|
278
|
+
`Packages selected: ${input.sources.length}`,
|
|
279
|
+
...input.sources.map((source) => `Package source: ${source}`),
|
|
280
|
+
`Packages removed: ${input.succeededSources.length}`,
|
|
281
|
+
...(input.failedSources.length > 0
|
|
282
|
+
? [`Packages failed: ${input.failedSources.length}`]
|
|
283
|
+
: ["Run /reload to deactivate removed package resources."]),
|
|
284
|
+
...(input.reason ? [`Latest failure detail: ${input.reason}`] : []),
|
|
285
|
+
],
|
|
286
|
+
lineTone: "dim",
|
|
287
|
+
output: input.output,
|
|
288
|
+
outputLabel:
|
|
289
|
+
input.outcome === "succeeded" ? "Uninstall output:" : "Error detail:",
|
|
290
|
+
outputDescription:
|
|
291
|
+
input.outcome === "succeeded" ? "uninstall output" : "error detail",
|
|
206
292
|
outputTone: "dim",
|
|
207
293
|
hideOutputWhenCollapsed: true,
|
|
208
294
|
}
|
|
@@ -227,6 +313,20 @@ export function createAutomaticUpdateWidgetLines(state: WidgetState): string[] {
|
|
|
227
313
|
]
|
|
228
314
|
}
|
|
229
315
|
|
|
316
|
+
if (state.mode === "package-installing") {
|
|
317
|
+
return [
|
|
318
|
+
"Pi package install in progress.",
|
|
319
|
+
`Installing package from ${state.source}...`,
|
|
320
|
+
]
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (state.mode === "package-uninstalling") {
|
|
324
|
+
return [
|
|
325
|
+
"Pi package uninstall in progress.",
|
|
326
|
+
`Removing ${state.current}/${state.total}: ${state.source}`,
|
|
327
|
+
]
|
|
328
|
+
}
|
|
329
|
+
|
|
230
330
|
return [
|
|
231
331
|
formatAutomaticUpdateHeadline("succeeded"),
|
|
232
332
|
`Reloading in ${state.secondsRemaining} second${state.secondsRemaining === 1 ? "" : "s"} to activate updated package resources.`,
|
|
@@ -304,10 +404,13 @@ export function getExecDisplayOutput(result: ExecResult): string | undefined {
|
|
|
304
404
|
return sections.length > 0 ? sections.join("\n\n") : undefined
|
|
305
405
|
}
|
|
306
406
|
|
|
307
|
-
export function getExecFailureDetail(
|
|
407
|
+
export function getExecFailureDetail(
|
|
408
|
+
result: ExecResult,
|
|
409
|
+
fallback = "Package command failed.",
|
|
410
|
+
): string {
|
|
308
411
|
const stderr = result.stderr.trim()
|
|
309
412
|
const stdout = result.stdout.trim()
|
|
310
|
-
const detail = stderr || stdout ||
|
|
413
|
+
const detail = stderr || stdout || fallback
|
|
311
414
|
|
|
312
415
|
return detail.length > 400 ? `${detail.slice(0, 397)}...` : detail
|
|
313
416
|
}
|
package/internal/runtime.ts
CHANGED
|
@@ -8,17 +8,33 @@ import {
|
|
|
8
8
|
type ExtensionContext,
|
|
9
9
|
} from "@earendil-works/pi-coding-agent"
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
INSTALL_COMMAND,
|
|
13
|
+
UNINSTALL_COMMAND,
|
|
14
|
+
UPDATE_COMMAND,
|
|
15
|
+
type ConfiguredPackageOption,
|
|
16
|
+
} from "./model.ts"
|
|
12
17
|
|
|
13
18
|
export interface PackageManagerDeps {
|
|
14
19
|
nowIso(): string
|
|
15
20
|
sleep(milliseconds: number): Promise<void>
|
|
16
21
|
isOffline(): boolean
|
|
17
22
|
checkForAvailableUpdates(ctx: ExtensionContext): Promise<string[]>
|
|
23
|
+
listConfiguredPackages(ctx: ExtensionContext): Promise<ConfiguredPackageOption[]>
|
|
18
24
|
runNativeUpdate(
|
|
19
25
|
pi: Pick<ExtensionAPI, "exec">,
|
|
20
26
|
ctx: ExtensionCommandContext,
|
|
21
27
|
): Promise<ExecResult>
|
|
28
|
+
runNativeInstall(
|
|
29
|
+
pi: Pick<ExtensionAPI, "exec">,
|
|
30
|
+
ctx: ExtensionCommandContext,
|
|
31
|
+
source: string,
|
|
32
|
+
): Promise<ExecResult>
|
|
33
|
+
runNativeUninstall(
|
|
34
|
+
pi: Pick<ExtensionAPI, "exec">,
|
|
35
|
+
ctx: ExtensionCommandContext,
|
|
36
|
+
source: string,
|
|
37
|
+
): Promise<ExecResult>
|
|
22
38
|
}
|
|
23
39
|
|
|
24
40
|
export const defaultPackageManagerDeps: PackageManagerDeps = {
|
|
@@ -30,25 +46,62 @@ export const defaultPackageManagerDeps: PackageManagerDeps = {
|
|
|
30
46
|
},
|
|
31
47
|
isOffline: () => Boolean(process.env.PI_OFFLINE),
|
|
32
48
|
async checkForAvailableUpdates(ctx: ExtensionContext): Promise<string[]> {
|
|
33
|
-
const
|
|
34
|
-
const settingsManager = SettingsManager.create(ctx.cwd, agentDir, {
|
|
35
|
-
projectTrusted: ctx.isProjectTrusted(),
|
|
36
|
-
})
|
|
37
|
-
const packageManager = new DefaultPackageManager({
|
|
38
|
-
cwd: ctx.cwd,
|
|
39
|
-
agentDir,
|
|
40
|
-
settingsManager,
|
|
41
|
-
})
|
|
49
|
+
const packageManager = createDefaultPackageManager(ctx)
|
|
42
50
|
const updates = await packageManager.checkForAvailableUpdates()
|
|
43
51
|
|
|
44
52
|
return updates
|
|
45
53
|
.map((update) => update.displayName)
|
|
46
54
|
.sort((left, right) => left.localeCompare(right))
|
|
47
55
|
},
|
|
56
|
+
async listConfiguredPackages(
|
|
57
|
+
ctx: ExtensionContext,
|
|
58
|
+
): Promise<ConfiguredPackageOption[]> {
|
|
59
|
+
return createDefaultPackageManager(ctx)
|
|
60
|
+
.listConfiguredPackages()
|
|
61
|
+
.map((pkg) => ({
|
|
62
|
+
source: pkg.source,
|
|
63
|
+
scope: pkg.scope,
|
|
64
|
+
filtered: pkg.filtered,
|
|
65
|
+
}))
|
|
66
|
+
.sort((left, right) => left.source.localeCompare(right.source))
|
|
67
|
+
},
|
|
48
68
|
runNativeUpdate(pi: Pick<ExtensionAPI, "exec">, ctx: ExtensionCommandContext) {
|
|
49
69
|
return pi.exec("pi", [...UPDATE_COMMAND], {
|
|
50
70
|
cwd: ctx.cwd,
|
|
51
71
|
signal: ctx.signal,
|
|
52
72
|
})
|
|
53
73
|
},
|
|
74
|
+
runNativeInstall(
|
|
75
|
+
pi: Pick<ExtensionAPI, "exec">,
|
|
76
|
+
ctx: ExtensionCommandContext,
|
|
77
|
+
source: string,
|
|
78
|
+
) {
|
|
79
|
+
return pi.exec("pi", [...INSTALL_COMMAND, source], {
|
|
80
|
+
cwd: ctx.cwd,
|
|
81
|
+
signal: ctx.signal,
|
|
82
|
+
})
|
|
83
|
+
},
|
|
84
|
+
runNativeUninstall(
|
|
85
|
+
pi: Pick<ExtensionAPI, "exec">,
|
|
86
|
+
ctx: ExtensionCommandContext,
|
|
87
|
+
source: string,
|
|
88
|
+
) {
|
|
89
|
+
return pi.exec("pi", [...UNINSTALL_COMMAND, source], {
|
|
90
|
+
cwd: ctx.cwd,
|
|
91
|
+
signal: ctx.signal,
|
|
92
|
+
})
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function createDefaultPackageManager(ctx: ExtensionContext): DefaultPackageManager {
|
|
97
|
+
const agentDir = getAgentDir()
|
|
98
|
+
const settingsManager = SettingsManager.create(ctx.cwd, agentDir, {
|
|
99
|
+
projectTrusted: ctx.isProjectTrusted(),
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
return new DefaultPackageManager({
|
|
103
|
+
cwd: ctx.cwd,
|
|
104
|
+
agentDir,
|
|
105
|
+
settingsManager,
|
|
106
|
+
})
|
|
54
107
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DynamicBorder,
|
|
3
|
+
type ExtensionCommandContext,
|
|
4
|
+
type Theme,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent"
|
|
6
|
+
import {
|
|
7
|
+
Container,
|
|
8
|
+
Key,
|
|
9
|
+
SelectList,
|
|
10
|
+
Text,
|
|
11
|
+
type Component,
|
|
12
|
+
type SelectItem,
|
|
13
|
+
type TUI,
|
|
14
|
+
matchesKey,
|
|
15
|
+
} from "@earendil-works/pi-tui"
|
|
16
|
+
|
|
17
|
+
import type { ConfiguredPackageOption } from "./model.ts"
|
|
18
|
+
|
|
19
|
+
export async function promptForPackagesToUninstall(
|
|
20
|
+
ctx: Pick<ExtensionCommandContext, "ui">,
|
|
21
|
+
packages: ConfiguredPackageOption[],
|
|
22
|
+
): Promise<string[] | undefined> {
|
|
23
|
+
return ctx.ui.custom<string[] | undefined>(
|
|
24
|
+
(tui, theme, _keybindings, done) => {
|
|
25
|
+
const picker = new PackageUninstallPicker(tui, theme, packages)
|
|
26
|
+
picker.done = done
|
|
27
|
+
return picker
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
overlay: true,
|
|
31
|
+
overlayOptions: {
|
|
32
|
+
width: "70%",
|
|
33
|
+
minWidth: 48,
|
|
34
|
+
maxHeight: "80%",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class PackageUninstallPicker implements Component {
|
|
41
|
+
private readonly container = new Container()
|
|
42
|
+
private readonly title = new Text("", 1, 0)
|
|
43
|
+
private readonly summary = new Text("", 1, 0)
|
|
44
|
+
private readonly footer = new Text("", 1, 0)
|
|
45
|
+
private readonly checkedSources = new Set<string>()
|
|
46
|
+
private readonly items: SelectItem[]
|
|
47
|
+
private readonly selectList: SelectList
|
|
48
|
+
private readonly tui: TUI
|
|
49
|
+
private readonly theme: Theme
|
|
50
|
+
|
|
51
|
+
constructor(tui: TUI, theme: Theme, packages: ConfiguredPackageOption[]) {
|
|
52
|
+
this.tui = tui
|
|
53
|
+
this.theme = theme
|
|
54
|
+
this.items = packages.map((pkg) => ({
|
|
55
|
+
value: pkg.source,
|
|
56
|
+
label: formatPackageOptionLabel(pkg.source, false),
|
|
57
|
+
}))
|
|
58
|
+
this.selectList = new SelectList(this.items, Math.min(this.items.length, 10), {
|
|
59
|
+
selectedPrefix: (text) => this.theme.fg("accent", text),
|
|
60
|
+
selectedText: (text) => this.theme.fg("accent", text),
|
|
61
|
+
description: (text) => this.theme.fg("muted", text),
|
|
62
|
+
scrollInfo: (text) => this.theme.fg("dim", text),
|
|
63
|
+
noMatch: (text) => this.theme.fg("warning", text),
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
this.selectList.onSelect = () => {
|
|
67
|
+
this.done?.(Array.from(this.checkedSources))
|
|
68
|
+
}
|
|
69
|
+
this.selectList.onCancel = () => {
|
|
70
|
+
this.done?.(undefined)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.container.addChild(
|
|
74
|
+
new DynamicBorder((text: string) => this.theme.fg("accent", text)),
|
|
75
|
+
)
|
|
76
|
+
this.container.addChild(this.title)
|
|
77
|
+
this.container.addChild(this.summary)
|
|
78
|
+
this.container.addChild(this.selectList)
|
|
79
|
+
this.container.addChild(this.footer)
|
|
80
|
+
this.container.addChild(
|
|
81
|
+
new DynamicBorder((text: string) => this.theme.fg("accent", text)),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
this.refreshText()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
done?: (value: string[] | undefined) => void
|
|
88
|
+
|
|
89
|
+
handleInput(data: string): void {
|
|
90
|
+
if (matchesKey(data, Key.space)) {
|
|
91
|
+
this.toggleSelectedItem()
|
|
92
|
+
this.tui.requestRender()
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.selectList.handleInput(data)
|
|
97
|
+
this.tui.requestRender()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
invalidate(): void {
|
|
101
|
+
this.container.invalidate()
|
|
102
|
+
this.refreshText()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
render(width: number): string[] {
|
|
106
|
+
return this.container.render(width)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private refreshText(): void {
|
|
110
|
+
this.title.setText(
|
|
111
|
+
this.theme.fg("accent", this.theme.bold("Select Pi Packages to Uninstall")),
|
|
112
|
+
)
|
|
113
|
+
this.summary.setText(
|
|
114
|
+
this.theme.fg(
|
|
115
|
+
"dim",
|
|
116
|
+
`${this.checkedSources.size} selected • enter confirm • esc cancel`,
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
this.footer.setText(this.theme.fg("dim", "↑↓ navigate • space toggle checkbox"))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private toggleSelectedItem(): void {
|
|
123
|
+
const selectedItem = this.selectList.getSelectedItem()
|
|
124
|
+
|
|
125
|
+
if (!selectedItem) {
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (this.checkedSources.has(selectedItem.value)) {
|
|
130
|
+
this.checkedSources.delete(selectedItem.value)
|
|
131
|
+
selectedItem.label = formatPackageOptionLabel(selectedItem.value, false)
|
|
132
|
+
} else {
|
|
133
|
+
this.checkedSources.add(selectedItem.value)
|
|
134
|
+
selectedItem.label = formatPackageOptionLabel(selectedItem.value, true)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.selectList.invalidate()
|
|
138
|
+
this.refreshText()
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function formatPackageOptionLabel(source: string, checked: boolean): string {
|
|
143
|
+
return `${checked ? "[x]" : "[ ]"} ${source}`
|
|
144
|
+
}
|