@gaosh3n/pi-package-manager 0.1.2 → 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 +11 -0
- package/index.ts +13 -3
- package/internal/controller.ts +126 -2
- package/internal/model.ts +14 -0
- package/internal/reports.ts +60 -2
- 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,
|
|
@@ -51,9 +52,9 @@ export default function initPackageManager(
|
|
|
51
52
|
|
|
52
53
|
pi.registerCommand("package-manager", {
|
|
53
54
|
description:
|
|
54
|
-
"Manage Pi packages (usage: /package-manager [status|update|install])",
|
|
55
|
+
"Manage Pi packages (usage: /package-manager [status|update|install|uninstall])",
|
|
55
56
|
getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
|
|
56
|
-
const items = ["status", "update", "install"].map((value) => ({
|
|
57
|
+
const items = ["status", "update", "install", "uninstall"].map((value) => ({
|
|
57
58
|
value,
|
|
58
59
|
label: value,
|
|
59
60
|
}))
|
|
@@ -84,7 +85,15 @@ export default function initPackageManager(
|
|
|
84
85
|
return
|
|
85
86
|
}
|
|
86
87
|
|
|
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
|
+
)
|
|
88
97
|
},
|
|
89
98
|
})
|
|
90
99
|
}
|
|
@@ -97,6 +106,7 @@ export {
|
|
|
97
106
|
createAutoUpdateRecord,
|
|
98
107
|
createAutoUpdateResultReport,
|
|
99
108
|
createInstallResultReport,
|
|
109
|
+
createUninstallResultReport,
|
|
100
110
|
createStatusReport,
|
|
101
111
|
formatStatusLines,
|
|
102
112
|
formatUtcTimestamp,
|
package/internal/controller.ts
CHANGED
|
@@ -17,11 +17,13 @@ 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">,
|
|
@@ -32,6 +34,7 @@ export function createPackageManagerController(
|
|
|
32
34
|
handleStatus,
|
|
33
35
|
handleUpdate,
|
|
34
36
|
handleInstall,
|
|
37
|
+
handleUninstall,
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
async function onSessionStart(
|
|
@@ -136,7 +139,7 @@ export function createPackageManagerController(
|
|
|
136
139
|
endedAtUtc: deps.nowIso(),
|
|
137
140
|
outcome: "failed",
|
|
138
141
|
packagesUpdated: 0,
|
|
139
|
-
reason: getExecFailureDetail(result),
|
|
142
|
+
reason: getExecFailureDetail(result, "Package update command failed."),
|
|
140
143
|
})
|
|
141
144
|
|
|
142
145
|
appendAutoUpdateRecordAndReport(
|
|
@@ -221,7 +224,10 @@ export function createPackageManagerController(
|
|
|
221
224
|
source,
|
|
222
225
|
outcome: "failed",
|
|
223
226
|
output,
|
|
224
|
-
reason: getExecFailureDetail(
|
|
227
|
+
reason: getExecFailureDetail(
|
|
228
|
+
result,
|
|
229
|
+
"Package install command failed.",
|
|
230
|
+
),
|
|
225
231
|
}),
|
|
226
232
|
)
|
|
227
233
|
} catch (error) {
|
|
@@ -240,6 +246,124 @@ export function createPackageManagerController(
|
|
|
240
246
|
}
|
|
241
247
|
}
|
|
242
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
|
+
|
|
243
367
|
function appendSkippedResult(startedAtUtc: string, reason: string): void {
|
|
244
368
|
const record = createAutoUpdateRecord({
|
|
245
369
|
startedAtUtc,
|
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
|
|
|
@@ -246,6 +247,53 @@ export function createInstallResultReport(input: {
|
|
|
246
247
|
}
|
|
247
248
|
}
|
|
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",
|
|
292
|
+
outputTone: "dim",
|
|
293
|
+
hideOutputWhenCollapsed: true,
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
249
297
|
export function createAutomaticUpdateWidgetLines(state: WidgetState): string[] {
|
|
250
298
|
if (state.mode === "status-checking") {
|
|
251
299
|
return ["Pi package status in progress.", "Checking for package updates..."]
|
|
@@ -272,6 +320,13 @@ export function createAutomaticUpdateWidgetLines(state: WidgetState): string[] {
|
|
|
272
320
|
]
|
|
273
321
|
}
|
|
274
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
|
+
|
|
275
330
|
return [
|
|
276
331
|
formatAutomaticUpdateHeadline("succeeded"),
|
|
277
332
|
`Reloading in ${state.secondsRemaining} second${state.secondsRemaining === 1 ? "" : "s"} to activate updated package resources.`,
|
|
@@ -349,10 +404,13 @@ export function getExecDisplayOutput(result: ExecResult): string | undefined {
|
|
|
349
404
|
return sections.length > 0 ? sections.join("\n\n") : undefined
|
|
350
405
|
}
|
|
351
406
|
|
|
352
|
-
export function getExecFailureDetail(
|
|
407
|
+
export function getExecFailureDetail(
|
|
408
|
+
result: ExecResult,
|
|
409
|
+
fallback = "Package command failed.",
|
|
410
|
+
): string {
|
|
353
411
|
const stderr = result.stderr.trim()
|
|
354
412
|
const stdout = result.stdout.trim()
|
|
355
|
-
const detail = stderr || stdout ||
|
|
413
|
+
const detail = stderr || stdout || fallback
|
|
356
414
|
|
|
357
415
|
return detail.length > 400 ? `${detail.slice(0, 397)}...` : detail
|
|
358
416
|
}
|
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
|
+
}
|