@gaosh3n/pi-package-manager 0.1.2 → 0.1.4
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 +14 -3
- package/internal/controller.ts +246 -63
- package/internal/model.ts +14 -0
- package/internal/reports.ts +176 -110
- package/internal/runtime.ts +48 -10
- package/internal/uninstall-picker.ts +144 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
- `/package-manager status` to see whether updates are available
|
|
10
10
|
- `/package-manager update` to run the update flow on demand
|
|
11
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
|
|
12
13
|
- a final result card in Pi so you can review the latest outcome
|
|
13
14
|
- automatic Pi reload after a successful startup update
|
|
14
15
|
|
|
@@ -55,3 +56,13 @@ Run:
|
|
|
55
56
|
```
|
|
56
57
|
|
|
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
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
createAutoUpdateResultReport,
|
|
28
28
|
createInstallResultReport,
|
|
29
29
|
createReportEntryRenderer,
|
|
30
|
+
createUninstallResultReport,
|
|
30
31
|
createStatusReport,
|
|
31
32
|
formatStatusLines,
|
|
32
33
|
formatUtcTimestamp,
|
|
@@ -48,12 +49,13 @@ export default function initPackageManager(
|
|
|
48
49
|
)
|
|
49
50
|
|
|
50
51
|
pi.on("session_start", controller.onSessionStart)
|
|
52
|
+
pi.on("session_shutdown", controller.onSessionShutdown)
|
|
51
53
|
|
|
52
54
|
pi.registerCommand("package-manager", {
|
|
53
55
|
description:
|
|
54
|
-
"Manage Pi packages (usage: /package-manager [status|update|install])",
|
|
56
|
+
"Manage Pi packages (usage: /package-manager [status|update|install|uninstall])",
|
|
55
57
|
getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
|
|
56
|
-
const items = ["status", "update", "install"].map((value) => ({
|
|
58
|
+
const items = ["status", "update", "install", "uninstall"].map((value) => ({
|
|
57
59
|
value,
|
|
58
60
|
label: value,
|
|
59
61
|
}))
|
|
@@ -84,7 +86,15 @@ export default function initPackageManager(
|
|
|
84
86
|
return
|
|
85
87
|
}
|
|
86
88
|
|
|
87
|
-
|
|
89
|
+
if (subcommand === "uninstall") {
|
|
90
|
+
await controller.handleUninstall(ctx)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
ctx.ui.notify(
|
|
95
|
+
"Usage: /package-manager [status|update|install|uninstall]",
|
|
96
|
+
"warning",
|
|
97
|
+
)
|
|
88
98
|
},
|
|
89
99
|
})
|
|
90
100
|
}
|
|
@@ -97,6 +107,7 @@ export {
|
|
|
97
107
|
createAutoUpdateRecord,
|
|
98
108
|
createAutoUpdateResultReport,
|
|
99
109
|
createInstallResultReport,
|
|
110
|
+
createUninstallResultReport,
|
|
100
111
|
createStatusReport,
|
|
101
112
|
formatStatusLines,
|
|
102
113
|
formatUtcTimestamp,
|
package/internal/controller.ts
CHANGED
|
@@ -17,27 +17,35 @@ import {
|
|
|
17
17
|
createInstallResultReport,
|
|
18
18
|
createStatusErrorReport,
|
|
19
19
|
createStatusReport,
|
|
20
|
+
createUninstallResultReport,
|
|
20
21
|
getExecDisplayOutput,
|
|
21
22
|
getExecFailureDetail,
|
|
22
23
|
setPackageManagerWidget,
|
|
23
24
|
} from "./reports.ts"
|
|
24
25
|
import { defaultPackageManagerDeps, type PackageManagerDeps } from "./runtime.ts"
|
|
26
|
+
import { promptForPackagesToUninstall } from "./uninstall-picker.ts"
|
|
25
27
|
|
|
26
28
|
export function createPackageManagerController(
|
|
27
29
|
pi: Pick<ExtensionAPI, "appendEntry" | "sendUserMessage" | "exec">,
|
|
28
30
|
deps: PackageManagerDeps = defaultPackageManagerDeps,
|
|
29
31
|
) {
|
|
32
|
+
let sessionIsActive = true
|
|
33
|
+
|
|
30
34
|
return {
|
|
31
35
|
onSessionStart,
|
|
36
|
+
onSessionShutdown,
|
|
32
37
|
handleStatus,
|
|
33
38
|
handleUpdate,
|
|
34
39
|
handleInstall,
|
|
40
|
+
handleUninstall,
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
async function onSessionStart(
|
|
38
44
|
event: Pick<SessionStartEvent, "reason">,
|
|
39
45
|
_ctx: ExtensionContext,
|
|
40
46
|
): Promise<void> {
|
|
47
|
+
sessionIsActive = true
|
|
48
|
+
|
|
41
49
|
if (!shouldAutoUpdateOnSessionStart(event)) {
|
|
42
50
|
return
|
|
43
51
|
}
|
|
@@ -47,6 +55,10 @@ export function createPackageManagerController(
|
|
|
47
55
|
})
|
|
48
56
|
}
|
|
49
57
|
|
|
58
|
+
async function onSessionShutdown(): Promise<void> {
|
|
59
|
+
sessionIsActive = false
|
|
60
|
+
}
|
|
61
|
+
|
|
50
62
|
async function handleStatus(ctx: ExtensionCommandContext): Promise<void> {
|
|
51
63
|
const lastAutoUpdate = getLastAutoUpdateRecord(ctx.sessionManager.getEntries())
|
|
52
64
|
|
|
@@ -54,26 +66,37 @@ export function createPackageManagerController(
|
|
|
54
66
|
|
|
55
67
|
try {
|
|
56
68
|
const availableUpdates = await deps.checkForAvailableUpdates(ctx)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
69
|
+
|
|
70
|
+
if (!isSessionActive()) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
appendReport({
|
|
75
|
+
type: REPORT_ENTRY_TYPE,
|
|
76
|
+
report: createStatusReport({
|
|
60
77
|
availableUpdates,
|
|
61
78
|
lastAutoUpdate,
|
|
62
79
|
}),
|
|
63
|
-
)
|
|
80
|
+
})
|
|
64
81
|
} catch (error) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
82
|
+
if (!isSessionActive()) {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
appendReport({
|
|
87
|
+
type: REPORT_ENTRY_TYPE,
|
|
88
|
+
report: createStatusErrorReport(
|
|
68
89
|
{
|
|
69
90
|
availableUpdates: [],
|
|
70
91
|
lastAutoUpdate,
|
|
71
92
|
},
|
|
72
93
|
getErrorMessage(error),
|
|
73
94
|
),
|
|
74
|
-
)
|
|
95
|
+
})
|
|
75
96
|
} finally {
|
|
76
|
-
|
|
97
|
+
if (isSessionActive()) {
|
|
98
|
+
clearPackageManagerWidget(ctx)
|
|
99
|
+
}
|
|
77
100
|
}
|
|
78
101
|
}
|
|
79
102
|
|
|
@@ -94,6 +117,10 @@ export function createPackageManagerController(
|
|
|
94
117
|
|
|
95
118
|
const availableUpdates = await deps.checkForAvailableUpdates(ctx)
|
|
96
119
|
|
|
120
|
+
if (!isSessionActive()) {
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
97
124
|
if (availableUpdates.length === 0) {
|
|
98
125
|
appendSkippedResult(startedAtUtc, "No package updates are available.")
|
|
99
126
|
return
|
|
@@ -105,6 +132,11 @@ export function createPackageManagerController(
|
|
|
105
132
|
})
|
|
106
133
|
|
|
107
134
|
const result = await deps.runNativeUpdate(pi, ctx)
|
|
135
|
+
|
|
136
|
+
if (!isSessionActive()) {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
108
140
|
const output = getExecDisplayOutput(result)
|
|
109
141
|
|
|
110
142
|
if (result.code === 0) {
|
|
@@ -114,17 +146,18 @@ export function createPackageManagerController(
|
|
|
114
146
|
outcome: "succeeded",
|
|
115
147
|
packagesUpdated: availableUpdates.length,
|
|
116
148
|
})
|
|
117
|
-
|
|
118
|
-
appendAutoUpdateRecordAndReport(
|
|
119
|
-
pi,
|
|
149
|
+
const report = createAutoUpdateResultReport({
|
|
120
150
|
record,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
await runReloadCountdown(ctx)
|
|
151
|
+
output,
|
|
152
|
+
reloadAfterSeconds: RELOAD_COUNTDOWN_SECONDS,
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
appendAutoUpdateRecordAndReport(pi, record, report)
|
|
156
|
+
|
|
157
|
+
if (!(await runReloadCountdown(ctx))) {
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
128
161
|
clearPackageManagerWidget(ctx)
|
|
129
162
|
shouldClearWidget = false
|
|
130
163
|
await ctx.reload()
|
|
@@ -136,16 +169,17 @@ export function createPackageManagerController(
|
|
|
136
169
|
endedAtUtc: deps.nowIso(),
|
|
137
170
|
outcome: "failed",
|
|
138
171
|
packagesUpdated: 0,
|
|
139
|
-
reason: getExecFailureDetail(result),
|
|
172
|
+
reason: getExecFailureDetail(result, "Package update command failed."),
|
|
140
173
|
})
|
|
174
|
+
const report = createAutoUpdateResultReport({ record, output })
|
|
141
175
|
|
|
142
|
-
appendAutoUpdateRecordAndReport(
|
|
143
|
-
pi,
|
|
144
|
-
record,
|
|
145
|
-
createAutoUpdateResultReport({ record, output }),
|
|
146
|
-
)
|
|
176
|
+
appendAutoUpdateRecordAndReport(pi, record, report)
|
|
147
177
|
notifyStartupFailure(ctx, options.startupTriggered)
|
|
148
178
|
} catch (error) {
|
|
179
|
+
if (!isSessionActive()) {
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
149
183
|
const record = createAutoUpdateRecord({
|
|
150
184
|
startedAtUtc,
|
|
151
185
|
endedAtUtc: deps.nowIso(),
|
|
@@ -153,15 +187,12 @@ export function createPackageManagerController(
|
|
|
153
187
|
packagesUpdated: 0,
|
|
154
188
|
reason: getErrorMessage(error),
|
|
155
189
|
})
|
|
190
|
+
const report = createAutoUpdateResultReport({ record })
|
|
156
191
|
|
|
157
|
-
appendAutoUpdateRecordAndReport(
|
|
158
|
-
pi,
|
|
159
|
-
record,
|
|
160
|
-
createAutoUpdateResultReport({ record }),
|
|
161
|
-
)
|
|
192
|
+
appendAutoUpdateRecordAndReport(pi, record, report)
|
|
162
193
|
notifyStartupFailure(ctx, options.startupTriggered)
|
|
163
194
|
} finally {
|
|
164
|
-
if (shouldClearWidget) {
|
|
195
|
+
if (shouldClearWidget && isSessionActive()) {
|
|
165
196
|
clearPackageManagerWidget(ctx)
|
|
166
197
|
}
|
|
167
198
|
}
|
|
@@ -183,7 +214,7 @@ export function createPackageManagerController(
|
|
|
183
214
|
)
|
|
184
215
|
)?.trim()
|
|
185
216
|
|
|
186
|
-
if (source === undefined) {
|
|
217
|
+
if (!isSessionActive() || source === undefined) {
|
|
187
218
|
return
|
|
188
219
|
}
|
|
189
220
|
|
|
@@ -197,46 +228,180 @@ export function createPackageManagerController(
|
|
|
197
228
|
|
|
198
229
|
try {
|
|
199
230
|
const result = await deps.runNativeInstall(pi, ctx, source)
|
|
231
|
+
|
|
232
|
+
if (!isSessionActive()) {
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
|
|
200
236
|
const output = getExecDisplayOutput(result)
|
|
237
|
+
const report = createInstallResultReport({
|
|
238
|
+
startedAtUtc,
|
|
239
|
+
endedAtUtc: deps.nowIso(),
|
|
240
|
+
source,
|
|
241
|
+
outcome: result.code === 0 ? "succeeded" : "failed",
|
|
242
|
+
output,
|
|
243
|
+
reason:
|
|
244
|
+
result.code === 0
|
|
245
|
+
? undefined
|
|
246
|
+
: getExecFailureDetail(
|
|
247
|
+
result,
|
|
248
|
+
"Package install command failed.",
|
|
249
|
+
),
|
|
250
|
+
})
|
|
201
251
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
createInstallResultReport({
|
|
206
|
-
startedAtUtc,
|
|
207
|
-
endedAtUtc: deps.nowIso(),
|
|
208
|
-
source,
|
|
209
|
-
outcome: "succeeded",
|
|
210
|
-
output,
|
|
211
|
-
}),
|
|
212
|
-
)
|
|
252
|
+
appendReport({ type: REPORT_ENTRY_TYPE, report })
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (!isSessionActive()) {
|
|
213
255
|
return
|
|
214
256
|
}
|
|
215
257
|
|
|
216
|
-
|
|
217
|
-
REPORT_ENTRY_TYPE,
|
|
218
|
-
createInstallResultReport({
|
|
258
|
+
appendReport({
|
|
259
|
+
type: REPORT_ENTRY_TYPE,
|
|
260
|
+
report: createInstallResultReport({
|
|
219
261
|
startedAtUtc,
|
|
220
262
|
endedAtUtc: deps.nowIso(),
|
|
221
263
|
source,
|
|
222
264
|
outcome: "failed",
|
|
223
|
-
|
|
224
|
-
reason: getExecFailureDetail(result),
|
|
265
|
+
reason: getErrorMessage(error),
|
|
225
266
|
}),
|
|
226
|
-
)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
267
|
+
})
|
|
268
|
+
} finally {
|
|
269
|
+
if (isSessionActive()) {
|
|
270
|
+
clearPackageManagerWidget(ctx)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function handleUninstall(ctx: ExtensionCommandContext): Promise<void> {
|
|
276
|
+
if (ctx.mode !== "tui") {
|
|
277
|
+
ctx.ui.notify("/package-manager uninstall requires TUI mode.", "warning")
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const packages = await deps.listConfiguredPackages(ctx)
|
|
282
|
+
|
|
283
|
+
if (!isSessionActive()) {
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (packages.length === 0) {
|
|
288
|
+
ctx.ui.notify("No Pi packages are available to uninstall.", "info")
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const selectedSources = await promptForPackagesToUninstall(ctx, packages)
|
|
293
|
+
|
|
294
|
+
if (!isSessionActive() || selectedSources === undefined) {
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (selectedSources.length === 0) {
|
|
299
|
+
ctx.ui.notify("Select at least one package to uninstall.", "warning")
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const selectedSourceSet = new Set(selectedSources)
|
|
304
|
+
const sources = packages
|
|
305
|
+
.map((pkg) => pkg.source)
|
|
306
|
+
.filter((source) => selectedSourceSet.has(source))
|
|
307
|
+
|
|
308
|
+
if (sources.length === 0) {
|
|
309
|
+
ctx.ui.notify("Select at least one package to uninstall.", "warning")
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const startedAtUtc = deps.nowIso()
|
|
314
|
+
const succeededSources: string[] = []
|
|
315
|
+
const failedSources: string[] = []
|
|
316
|
+
const outputSections: string[] = []
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
for (const [index, source] of sources.entries()) {
|
|
320
|
+
setPackageManagerWidget(ctx, {
|
|
321
|
+
mode: "package-uninstalling",
|
|
322
|
+
current: index + 1,
|
|
323
|
+
total: sources.length,
|
|
324
|
+
source,
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
const result = await deps.runNativeUninstall(pi, ctx, source)
|
|
328
|
+
|
|
329
|
+
if (!isSessionActive()) {
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const output = getExecDisplayOutput(result)
|
|
334
|
+
|
|
335
|
+
if (result.code === 0) {
|
|
336
|
+
if (output) {
|
|
337
|
+
outputSections.push(`[${source}]\n${output}`)
|
|
338
|
+
}
|
|
339
|
+
succeededSources.push(source)
|
|
340
|
+
continue
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
failedSources.push(source)
|
|
344
|
+
outputSections.push(
|
|
345
|
+
`[${source}]\n${output ?? getExecFailureDetail(result, "Package uninstall command failed.")}`,
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
appendReport({
|
|
350
|
+
type: REPORT_ENTRY_TYPE,
|
|
351
|
+
report: createUninstallResultReport({
|
|
231
352
|
startedAtUtc,
|
|
232
353
|
endedAtUtc: deps.nowIso(),
|
|
233
|
-
|
|
234
|
-
outcome:
|
|
235
|
-
|
|
354
|
+
sources,
|
|
355
|
+
outcome:
|
|
356
|
+
failedSources.length === 0
|
|
357
|
+
? "succeeded"
|
|
358
|
+
: succeededSources.length > 0
|
|
359
|
+
? "partial"
|
|
360
|
+
: "failed",
|
|
361
|
+
succeededSources,
|
|
362
|
+
failedSources,
|
|
363
|
+
output:
|
|
364
|
+
outputSections.length > 0
|
|
365
|
+
? outputSections.join("\n\n")
|
|
366
|
+
: undefined,
|
|
367
|
+
reason:
|
|
368
|
+
failedSources.length > 0
|
|
369
|
+
? `Failed to uninstall ${failedSources[0]}.`
|
|
370
|
+
: undefined,
|
|
236
371
|
}),
|
|
372
|
+
})
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (!isSessionActive()) {
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const failedSource = sources[succeededSources.length]
|
|
379
|
+
const errorMessage = getErrorMessage(error)
|
|
380
|
+
|
|
381
|
+
if (failedSource && !failedSources.includes(failedSource)) {
|
|
382
|
+
failedSources.push(failedSource)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
outputSections.push(
|
|
386
|
+
failedSource ? `[${failedSource}]\n${errorMessage}` : errorMessage,
|
|
237
387
|
)
|
|
388
|
+
appendReport({
|
|
389
|
+
type: REPORT_ENTRY_TYPE,
|
|
390
|
+
report: createUninstallResultReport({
|
|
391
|
+
startedAtUtc,
|
|
392
|
+
endedAtUtc: deps.nowIso(),
|
|
393
|
+
sources,
|
|
394
|
+
outcome: succeededSources.length > 0 ? "partial" : "failed",
|
|
395
|
+
succeededSources,
|
|
396
|
+
failedSources,
|
|
397
|
+
output: outputSections.join("\n\n"),
|
|
398
|
+
reason: errorMessage,
|
|
399
|
+
}),
|
|
400
|
+
})
|
|
238
401
|
} finally {
|
|
239
|
-
|
|
402
|
+
if (isSessionActive()) {
|
|
403
|
+
clearPackageManagerWidget(ctx)
|
|
404
|
+
}
|
|
240
405
|
}
|
|
241
406
|
}
|
|
242
407
|
|
|
@@ -248,26 +413,44 @@ export function createPackageManagerController(
|
|
|
248
413
|
packagesUpdated: 0,
|
|
249
414
|
reason,
|
|
250
415
|
})
|
|
416
|
+
const report = createAutoUpdateResultReport({ record })
|
|
251
417
|
|
|
252
|
-
appendAutoUpdateRecordAndReport(
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
418
|
+
appendAutoUpdateRecordAndReport(pi, record, report)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function appendReport(entry: {
|
|
422
|
+
type: string
|
|
423
|
+
report: ReturnType<typeof createStatusReport>
|
|
424
|
+
}): void {
|
|
425
|
+
pi.appendEntry(entry.type, entry.report)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function isSessionActive(): boolean {
|
|
429
|
+
return sessionIsActive
|
|
257
430
|
}
|
|
258
431
|
|
|
259
432
|
async function runReloadCountdown(
|
|
260
433
|
ctx: ExtensionContext,
|
|
261
434
|
seconds = RELOAD_COUNTDOWN_SECONDS,
|
|
262
|
-
): Promise<
|
|
435
|
+
): Promise<boolean> {
|
|
263
436
|
for (let remaining = seconds; remaining >= 1; remaining--) {
|
|
437
|
+
if (!isSessionActive()) {
|
|
438
|
+
return false
|
|
439
|
+
}
|
|
440
|
+
|
|
264
441
|
setPackageManagerWidget(ctx, {
|
|
265
442
|
mode: "countdown",
|
|
266
443
|
secondsRemaining: remaining,
|
|
267
444
|
})
|
|
268
445
|
|
|
269
446
|
await deps.sleep(1000)
|
|
447
|
+
|
|
448
|
+
if (!isSessionActive()) {
|
|
449
|
+
return false
|
|
450
|
+
}
|
|
270
451
|
}
|
|
452
|
+
|
|
453
|
+
return true
|
|
271
454
|
}
|
|
272
455
|
}
|
|
273
456
|
|
package/internal/model.ts
CHANGED
|
@@ -6,9 +6,11 @@ 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
8
|
export const INSTALL_COMMAND = ["install"] as const
|
|
9
|
+
export const UNINSTALL_COMMAND = ["uninstall"] as const
|
|
9
10
|
|
|
10
11
|
export type AutoUpdateOutcome = "succeeded" | "failed" | "skipped"
|
|
11
12
|
export type InstallOutcome = "succeeded" | "failed"
|
|
13
|
+
export type UninstallOutcome = "succeeded" | "partial" | "failed"
|
|
12
14
|
export type ReportTone = "info" | "success" | "warning" | "error"
|
|
13
15
|
|
|
14
16
|
export type WidgetState =
|
|
@@ -17,6 +19,12 @@ export type WidgetState =
|
|
|
17
19
|
| { mode: "installing"; packages: number }
|
|
18
20
|
| { mode: "countdown"; secondsRemaining: number }
|
|
19
21
|
| { mode: "package-installing"; source: string }
|
|
22
|
+
| {
|
|
23
|
+
mode: "package-uninstalling"
|
|
24
|
+
current: number
|
|
25
|
+
total: number
|
|
26
|
+
source: string
|
|
27
|
+
}
|
|
20
28
|
|
|
21
29
|
export interface AutoUpdateRecord {
|
|
22
30
|
startedAtUtc: string
|
|
@@ -43,3 +51,9 @@ export interface PackageStatusSnapshot {
|
|
|
43
51
|
availableUpdates: string[]
|
|
44
52
|
lastAutoUpdate?: AutoUpdateRecord
|
|
45
53
|
}
|
|
54
|
+
|
|
55
|
+
export interface ConfiguredPackageOption {
|
|
56
|
+
source: string
|
|
57
|
+
scope: "user" | "project"
|
|
58
|
+
filtered: boolean
|
|
59
|
+
}
|
package/internal/reports.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type InstallOutcome,
|
|
15
15
|
type PackageManagerReport,
|
|
16
16
|
type PackageStatusSnapshot,
|
|
17
|
+
type UninstallOutcome,
|
|
17
18
|
type WidgetState,
|
|
18
19
|
} from "./model.ts"
|
|
19
20
|
|
|
@@ -35,81 +36,7 @@ export function createReportEntryRenderer() {
|
|
|
35
36
|
return undefined
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
report.tone === "error"
|
|
40
|
-
? "error"
|
|
41
|
-
: report.tone === "warning"
|
|
42
|
-
? "warning"
|
|
43
|
-
: report.tone === "success"
|
|
44
|
-
? "success"
|
|
45
|
-
: "accent"
|
|
46
|
-
const lineTextTone = report.lineTone === "dim" ? "dim" : "customMessageText"
|
|
47
|
-
const outputTextTone = report.outputTone === "dim" ? "dim" : lineTextTone
|
|
48
|
-
|
|
49
|
-
const box = new Box(1, 1, (text: string) => theme.bg("customMessageBg", text))
|
|
50
|
-
|
|
51
|
-
box.addChild(
|
|
52
|
-
new Text(
|
|
53
|
-
`${theme.fg(toneColor, "●")} ${theme.bold(theme.fg("customMessageLabel", report.title))}`,
|
|
54
|
-
0,
|
|
55
|
-
0,
|
|
56
|
-
),
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
if (report.headline) {
|
|
60
|
-
box.addChild(new Text(theme.fg("text", report.headline), 0, 0))
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (report.lines.length > 0) {
|
|
64
|
-
box.addChild(
|
|
65
|
-
new Text(
|
|
66
|
-
report.lines.map((line) => theme.fg(lineTextTone, line)).join("\n"),
|
|
67
|
-
0,
|
|
68
|
-
0,
|
|
69
|
-
),
|
|
70
|
-
)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (report.output?.trim()) {
|
|
74
|
-
if (!expanded && report.hideOutputWhenCollapsed) {
|
|
75
|
-
box.addChild(
|
|
76
|
-
new Text(
|
|
77
|
-
formatExpandHint(
|
|
78
|
-
theme,
|
|
79
|
-
`to expand to see ${report.outputDescription ?? "output"}.`,
|
|
80
|
-
),
|
|
81
|
-
0,
|
|
82
|
-
0,
|
|
83
|
-
),
|
|
84
|
-
)
|
|
85
|
-
} else {
|
|
86
|
-
const { text, truncated } = formatReportOutput(report.output, expanded)
|
|
87
|
-
|
|
88
|
-
box.addChild(
|
|
89
|
-
new Text(
|
|
90
|
-
theme.fg(outputTextTone, report.outputLabel ?? "Output:"),
|
|
91
|
-
0,
|
|
92
|
-
0,
|
|
93
|
-
),
|
|
94
|
-
)
|
|
95
|
-
box.addChild(new Text(theme.fg(outputTextTone, text), 0, 0))
|
|
96
|
-
|
|
97
|
-
if (truncated) {
|
|
98
|
-
box.addChild(
|
|
99
|
-
new Text(
|
|
100
|
-
formatExpandHint(
|
|
101
|
-
theme,
|
|
102
|
-
`to expand to view the full ${report.outputDescription ?? "output"}.`,
|
|
103
|
-
),
|
|
104
|
-
0,
|
|
105
|
-
0,
|
|
106
|
-
),
|
|
107
|
-
)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return box
|
|
39
|
+
return createReportBox(report, theme, { expanded })
|
|
113
40
|
}
|
|
114
41
|
}
|
|
115
42
|
|
|
@@ -246,6 +173,53 @@ export function createInstallResultReport(input: {
|
|
|
246
173
|
}
|
|
247
174
|
}
|
|
248
175
|
|
|
176
|
+
export function createUninstallResultReport(input: {
|
|
177
|
+
startedAtUtc: string
|
|
178
|
+
endedAtUtc: string
|
|
179
|
+
sources: string[]
|
|
180
|
+
outcome: UninstallOutcome
|
|
181
|
+
succeededSources: string[]
|
|
182
|
+
failedSources: string[]
|
|
183
|
+
output?: string
|
|
184
|
+
reason?: string
|
|
185
|
+
}): PackageManagerReport {
|
|
186
|
+
return {
|
|
187
|
+
title: PACKAGE_MANAGER_TITLE,
|
|
188
|
+
headline:
|
|
189
|
+
input.outcome === "succeeded"
|
|
190
|
+
? "Pi package uninstall completed."
|
|
191
|
+
: input.outcome === "partial"
|
|
192
|
+
? "Pi package uninstall partially completed."
|
|
193
|
+
: "Pi package uninstall failed.",
|
|
194
|
+
tone:
|
|
195
|
+
input.outcome === "succeeded"
|
|
196
|
+
? "success"
|
|
197
|
+
: input.outcome === "partial"
|
|
198
|
+
? "warning"
|
|
199
|
+
: "error",
|
|
200
|
+
lines: [
|
|
201
|
+
`Start: ${formatUtcTimestamp(input.startedAtUtc)}`,
|
|
202
|
+
`End: ${formatUtcTimestamp(input.endedAtUtc)}`,
|
|
203
|
+
`Result: ${input.outcome}`,
|
|
204
|
+
`Packages selected: ${input.sources.length}`,
|
|
205
|
+
...input.sources.map((source) => `Package source: ${source}`),
|
|
206
|
+
`Packages removed: ${input.succeededSources.length}`,
|
|
207
|
+
...(input.failedSources.length > 0
|
|
208
|
+
? [`Packages failed: ${input.failedSources.length}`]
|
|
209
|
+
: ["Run /reload to deactivate removed package resources."]),
|
|
210
|
+
...(input.reason ? [`Latest failure detail: ${input.reason}`] : []),
|
|
211
|
+
],
|
|
212
|
+
lineTone: "dim",
|
|
213
|
+
output: input.output,
|
|
214
|
+
outputLabel:
|
|
215
|
+
input.outcome === "succeeded" ? "Uninstall output:" : "Error detail:",
|
|
216
|
+
outputDescription:
|
|
217
|
+
input.outcome === "succeeded" ? "uninstall output" : "error detail",
|
|
218
|
+
outputTone: "dim",
|
|
219
|
+
hideOutputWhenCollapsed: true,
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
249
223
|
export function createAutomaticUpdateWidgetLines(state: WidgetState): string[] {
|
|
250
224
|
if (state.mode === "status-checking") {
|
|
251
225
|
return ["Pi package status in progress.", "Checking for package updates..."]
|
|
@@ -272,6 +246,13 @@ export function createAutomaticUpdateWidgetLines(state: WidgetState): string[] {
|
|
|
272
246
|
]
|
|
273
247
|
}
|
|
274
248
|
|
|
249
|
+
if (state.mode === "package-uninstalling") {
|
|
250
|
+
return [
|
|
251
|
+
"Pi package uninstall in progress.",
|
|
252
|
+
`Removing ${state.current}/${state.total}: ${state.source}`,
|
|
253
|
+
]
|
|
254
|
+
}
|
|
255
|
+
|
|
275
256
|
return [
|
|
276
257
|
formatAutomaticUpdateHeadline("succeeded"),
|
|
277
258
|
`Reloading in ${state.secondsRemaining} second${state.secondsRemaining === 1 ? "" : "s"} to activate updated package resources.`,
|
|
@@ -288,39 +269,12 @@ export function setPackageManagerWidget(
|
|
|
288
269
|
|
|
289
270
|
ctx.ui.setWidget(
|
|
290
271
|
PACKAGE_MANAGER_WIDGET_KEY,
|
|
291
|
-
(_tui, theme) =>
|
|
292
|
-
|
|
293
|
-
state.mode === "countdown"
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const titleColor = state.mode === "countdown" ? "success" : "accent"
|
|
298
|
-
const bodyLines = createAutomaticUpdateWidgetLines(state)
|
|
299
|
-
|
|
300
|
-
box.addChild(
|
|
301
|
-
new Text(
|
|
302
|
-
`${theme.fg(titleColor, "●")} ${theme.bold(theme.fg("customMessageLabel", PACKAGE_MANAGER_TITLE))}`,
|
|
303
|
-
0,
|
|
304
|
-
0,
|
|
305
|
-
),
|
|
306
|
-
)
|
|
307
|
-
box.addChild(
|
|
308
|
-
new Text(
|
|
309
|
-
bodyLines
|
|
310
|
-
.map((line, index) =>
|
|
311
|
-
theme.fg(
|
|
312
|
-
index === bodyLines.length - 1 ? "dim" : "text",
|
|
313
|
-
line,
|
|
314
|
-
),
|
|
315
|
-
)
|
|
316
|
-
.join("\n"),
|
|
317
|
-
0,
|
|
318
|
-
0,
|
|
319
|
-
),
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
return box
|
|
323
|
-
},
|
|
272
|
+
(_tui, theme) =>
|
|
273
|
+
createStatusBox(theme, createAutomaticUpdateWidgetLines(state), {
|
|
274
|
+
titleColor: state.mode === "countdown" ? "success" : "accent",
|
|
275
|
+
background:
|
|
276
|
+
state.mode === "countdown" ? "toolSuccessBg" : "toolPendingBg",
|
|
277
|
+
}),
|
|
324
278
|
{ placement: "aboveEditor" },
|
|
325
279
|
)
|
|
326
280
|
}
|
|
@@ -349,10 +303,13 @@ export function getExecDisplayOutput(result: ExecResult): string | undefined {
|
|
|
349
303
|
return sections.length > 0 ? sections.join("\n\n") : undefined
|
|
350
304
|
}
|
|
351
305
|
|
|
352
|
-
export function getExecFailureDetail(
|
|
306
|
+
export function getExecFailureDetail(
|
|
307
|
+
result: ExecResult,
|
|
308
|
+
fallback = "Package command failed.",
|
|
309
|
+
): string {
|
|
353
310
|
const stderr = result.stderr.trim()
|
|
354
311
|
const stdout = result.stdout.trim()
|
|
355
|
-
const detail = stderr || stdout ||
|
|
312
|
+
const detail = stderr || stdout || fallback
|
|
356
313
|
|
|
357
314
|
return detail.length > 400 ? `${detail.slice(0, 397)}...` : detail
|
|
358
315
|
}
|
|
@@ -370,6 +327,115 @@ function formatAutomaticUpdateHeadline(
|
|
|
370
327
|
return `Pi package(s) update ${suffix}.`
|
|
371
328
|
}
|
|
372
329
|
|
|
330
|
+
function createReportBox(
|
|
331
|
+
report: PackageManagerReport,
|
|
332
|
+
theme: ThemeLike,
|
|
333
|
+
options: { expanded: boolean },
|
|
334
|
+
): Box {
|
|
335
|
+
const toneColor =
|
|
336
|
+
report.tone === "error"
|
|
337
|
+
? "error"
|
|
338
|
+
: report.tone === "warning"
|
|
339
|
+
? "warning"
|
|
340
|
+
: report.tone === "success"
|
|
341
|
+
? "success"
|
|
342
|
+
: "accent"
|
|
343
|
+
const lineTextTone = report.lineTone === "dim" ? "dim" : "customMessageText"
|
|
344
|
+
const outputTextTone = report.outputTone === "dim" ? "dim" : lineTextTone
|
|
345
|
+
const box = new Box(1, 1, (text: string) => theme.bg("customMessageBg", text))
|
|
346
|
+
|
|
347
|
+
box.addChild(
|
|
348
|
+
new Text(
|
|
349
|
+
`${theme.fg(toneColor, "●")} ${theme.bold(theme.fg("customMessageLabel", report.title))}`,
|
|
350
|
+
0,
|
|
351
|
+
0,
|
|
352
|
+
),
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
if (report.headline) {
|
|
356
|
+
box.addChild(new Text(theme.fg("text", report.headline), 0, 0))
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (report.lines.length > 0) {
|
|
360
|
+
box.addChild(
|
|
361
|
+
new Text(
|
|
362
|
+
report.lines.map((line) => theme.fg(lineTextTone, line)).join("\n"),
|
|
363
|
+
0,
|
|
364
|
+
0,
|
|
365
|
+
),
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (!report.output?.trim()) {
|
|
370
|
+
return box
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (!options.expanded && report.hideOutputWhenCollapsed) {
|
|
374
|
+
box.addChild(
|
|
375
|
+
new Text(
|
|
376
|
+
formatExpandHint(
|
|
377
|
+
theme,
|
|
378
|
+
`to expand to see ${report.outputDescription ?? "output"}.`,
|
|
379
|
+
),
|
|
380
|
+
0,
|
|
381
|
+
0,
|
|
382
|
+
),
|
|
383
|
+
)
|
|
384
|
+
return box
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const { text, truncated } = formatReportOutput(report.output, options.expanded)
|
|
388
|
+
|
|
389
|
+
box.addChild(
|
|
390
|
+
new Text(theme.fg(outputTextTone, report.outputLabel ?? "Output:"), 0, 0),
|
|
391
|
+
)
|
|
392
|
+
box.addChild(new Text(theme.fg(outputTextTone, text), 0, 0))
|
|
393
|
+
|
|
394
|
+
if (truncated) {
|
|
395
|
+
box.addChild(
|
|
396
|
+
new Text(
|
|
397
|
+
formatExpandHint(
|
|
398
|
+
theme,
|
|
399
|
+
`to expand to view the full ${report.outputDescription ?? "output"}.`,
|
|
400
|
+
),
|
|
401
|
+
0,
|
|
402
|
+
0,
|
|
403
|
+
),
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return box
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function createStatusBox(
|
|
411
|
+
theme: ThemeLike,
|
|
412
|
+
bodyLines: string[],
|
|
413
|
+
options: { titleColor: string; background: string },
|
|
414
|
+
): Box {
|
|
415
|
+
const box = new Box(1, 1, (text: string) => theme.bg(options.background, text))
|
|
416
|
+
|
|
417
|
+
box.addChild(
|
|
418
|
+
new Text(
|
|
419
|
+
`${theme.fg(options.titleColor, "●")} ${theme.bold(theme.fg("customMessageLabel", PACKAGE_MANAGER_TITLE))}`,
|
|
420
|
+
0,
|
|
421
|
+
0,
|
|
422
|
+
),
|
|
423
|
+
)
|
|
424
|
+
box.addChild(
|
|
425
|
+
new Text(
|
|
426
|
+
bodyLines
|
|
427
|
+
.map((line, index) =>
|
|
428
|
+
theme.fg(index === bodyLines.length - 1 ? "dim" : "text", line),
|
|
429
|
+
)
|
|
430
|
+
.join("\n"),
|
|
431
|
+
0,
|
|
432
|
+
0,
|
|
433
|
+
),
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
return box
|
|
437
|
+
}
|
|
438
|
+
|
|
373
439
|
function formatExpandHint(theme: ThemeLike, description: string): string {
|
|
374
440
|
const expandKey = keyText("app.tools.expand") || "Ctrl+O"
|
|
375
441
|
|
package/internal/runtime.ts
CHANGED
|
@@ -8,13 +8,19 @@ 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,
|
|
@@ -24,6 +30,11 @@ export interface PackageManagerDeps {
|
|
|
24
30
|
ctx: ExtensionCommandContext,
|
|
25
31
|
source: string,
|
|
26
32
|
): Promise<ExecResult>
|
|
33
|
+
runNativeUninstall(
|
|
34
|
+
pi: Pick<ExtensionAPI, "exec">,
|
|
35
|
+
ctx: ExtensionCommandContext,
|
|
36
|
+
source: string,
|
|
37
|
+
): Promise<ExecResult>
|
|
27
38
|
}
|
|
28
39
|
|
|
29
40
|
export const defaultPackageManagerDeps: PackageManagerDeps = {
|
|
@@ -35,21 +46,25 @@ export const defaultPackageManagerDeps: PackageManagerDeps = {
|
|
|
35
46
|
},
|
|
36
47
|
isOffline: () => Boolean(process.env.PI_OFFLINE),
|
|
37
48
|
async checkForAvailableUpdates(ctx: ExtensionContext): Promise<string[]> {
|
|
38
|
-
const
|
|
39
|
-
const settingsManager = SettingsManager.create(ctx.cwd, agentDir, {
|
|
40
|
-
projectTrusted: ctx.isProjectTrusted(),
|
|
41
|
-
})
|
|
42
|
-
const packageManager = new DefaultPackageManager({
|
|
43
|
-
cwd: ctx.cwd,
|
|
44
|
-
agentDir,
|
|
45
|
-
settingsManager,
|
|
46
|
-
})
|
|
49
|
+
const packageManager = createDefaultPackageManager(ctx)
|
|
47
50
|
const updates = await packageManager.checkForAvailableUpdates()
|
|
48
51
|
|
|
49
52
|
return updates
|
|
50
53
|
.map((update) => update.displayName)
|
|
51
54
|
.sort((left, right) => left.localeCompare(right))
|
|
52
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
|
+
},
|
|
53
68
|
runNativeUpdate(pi: Pick<ExtensionAPI, "exec">, ctx: ExtensionCommandContext) {
|
|
54
69
|
return pi.exec("pi", [...UPDATE_COMMAND], {
|
|
55
70
|
cwd: ctx.cwd,
|
|
@@ -66,4 +81,27 @@ export const defaultPackageManagerDeps: PackageManagerDeps = {
|
|
|
66
81
|
signal: ctx.signal,
|
|
67
82
|
})
|
|
68
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
|
+
})
|
|
69
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
|
+
}
|