@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 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 package updates (usage: /package-manager [status|update])",
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
- ctx.ui.notify("Usage: /package-manager [status|update]", "warning")
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,
@@ -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
+ }
@@ -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(theme, "to expand to see update output."),
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
- "to expand to view the full update output.",
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(result: ExecResult): string {
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 || "Package update command failed."
413
+ const detail = stderr || stdout || fallback
311
414
 
312
415
  return detail.length > 400 ? `${detail.slice(0, 397)}...` : detail
313
416
  }
@@ -8,17 +8,33 @@ import {
8
8
  type ExtensionContext,
9
9
  } from "@earendil-works/pi-coding-agent"
10
10
 
11
- import { UPDATE_COMMAND } from "./model.ts"
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 agentDir = getAgentDir()
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaosh3n/pi-package-manager",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Package Manager for Pi",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.23.0",