@gaosh3n/pi-package-manager 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,396 @@
1
+ import {
2
+ keyText,
3
+ type ExtensionContext,
4
+ type ExecResult,
5
+ } from "@earendil-works/pi-coding-agent"
6
+ import { Box, Text } from "@earendil-works/pi-tui"
7
+
8
+ import {
9
+ OUTPUT_PREVIEW_LINE_COUNT,
10
+ PACKAGE_MANAGER_TITLE,
11
+ PACKAGE_MANAGER_WIDGET_KEY,
12
+ type AutoUpdateOutcome,
13
+ type AutoUpdateRecord,
14
+ type InstallOutcome,
15
+ type PackageManagerReport,
16
+ type PackageStatusSnapshot,
17
+ type WidgetState,
18
+ } from "./model.ts"
19
+
20
+ interface ThemeLike {
21
+ fg(token: string, text: string): string
22
+ bg(token: string, text: string): string
23
+ bold(text: string): string
24
+ }
25
+
26
+ export function createReportEntryRenderer() {
27
+ return (
28
+ entry: { data?: PackageManagerReport },
29
+ { expanded }: { expanded: boolean },
30
+ theme: ThemeLike,
31
+ ) => {
32
+ const report = entry.data
33
+
34
+ if (!report) {
35
+ return undefined
36
+ }
37
+
38
+ const toneColor =
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
113
+ }
114
+ }
115
+
116
+ export function formatUtcTimestamp(isoUtc: string): string {
117
+ return isoUtc.replace("T", " ").replace(/\.\d{3}Z$/, " UTC+00")
118
+ }
119
+
120
+ export function formatStatusLines(snapshot: PackageStatusSnapshot): string[] {
121
+ if (!snapshot.lastAutoUpdate) {
122
+ return ["Latest package update: none recorded."]
123
+ }
124
+
125
+ const lines = [
126
+ `Latest update start: ${formatUtcTimestamp(snapshot.lastAutoUpdate.startedAtUtc)}`,
127
+ `Latest update end: ${formatUtcTimestamp(snapshot.lastAutoUpdate.endedAtUtc)}`,
128
+ `Latest update result: ${snapshot.lastAutoUpdate.outcome}`,
129
+ `Latest update packages updated: ${snapshot.lastAutoUpdate.packagesUpdated}`,
130
+ ]
131
+
132
+ if (snapshot.lastAutoUpdate.reason) {
133
+ lines.push(`Latest update detail: ${snapshot.lastAutoUpdate.reason}`)
134
+ }
135
+
136
+ return lines
137
+ }
138
+
139
+ export function createStatusReport(
140
+ snapshot: PackageStatusSnapshot,
141
+ ): PackageManagerReport {
142
+ return {
143
+ title: PACKAGE_MANAGER_TITLE,
144
+ headline:
145
+ snapshot.availableUpdates.length === 0
146
+ ? "No package updates are available."
147
+ : `${snapshot.availableUpdates.length} package update${snapshot.availableUpdates.length === 1 ? " is" : "s are"} available.`,
148
+ tone: snapshot.availableUpdates.length > 0 ? "warning" : "info",
149
+ lines: formatStatusLines(snapshot),
150
+ lineTone: "dim",
151
+ output:
152
+ snapshot.availableUpdates.length > 0
153
+ ? snapshot.availableUpdates.map((name) => `- ${name}`).join("\n")
154
+ : undefined,
155
+ outputLabel:
156
+ snapshot.availableUpdates.length > 0 ? "Available updates:" : undefined,
157
+ outputTone: "dim",
158
+ }
159
+ }
160
+
161
+ export function createStatusErrorReport(
162
+ snapshot: PackageStatusSnapshot,
163
+ errorMessage: string,
164
+ ): PackageManagerReport {
165
+ return {
166
+ title: PACKAGE_MANAGER_TITLE,
167
+ headline: "Package status check failed.",
168
+ tone: "error",
169
+ lines: formatStatusLines(snapshot),
170
+ lineTone: "dim",
171
+ output: `Failed to check package updates: ${errorMessage}`,
172
+ outputLabel: "Error detail:",
173
+ outputTone: "dim",
174
+ }
175
+ }
176
+
177
+ export function createAutoUpdateResultReport(input: {
178
+ record: AutoUpdateRecord
179
+ output?: string
180
+ reloadAfterSeconds?: number
181
+ }): PackageManagerReport {
182
+ const lines = [
183
+ `Start: ${formatUtcTimestamp(input.record.startedAtUtc)}`,
184
+ `End: ${formatUtcTimestamp(input.record.endedAtUtc)}`,
185
+ `Result: ${input.record.outcome}`,
186
+ `Packages updated: ${input.record.packagesUpdated}`,
187
+ ]
188
+
189
+ if (input.reloadAfterSeconds) {
190
+ lines.push(
191
+ `Reloading in ${input.reloadAfterSeconds} seconds to activate updated package resources.`,
192
+ )
193
+ }
194
+
195
+ return {
196
+ title: PACKAGE_MANAGER_TITLE,
197
+ headline: formatAutomaticUpdateHeadline(input.record.outcome),
198
+ tone:
199
+ input.record.outcome === "failed"
200
+ ? "error"
201
+ : input.record.outcome === "succeeded"
202
+ ? "success"
203
+ : "info",
204
+ lines,
205
+ lineTone: "dim",
206
+ output: input.output ?? input.record.reason,
207
+ outputLabel: "Update output:",
208
+ outputDescription: "update output",
209
+ outputTone: "dim",
210
+ hideOutputWhenCollapsed: true,
211
+ }
212
+ }
213
+
214
+ export function createInstallResultReport(input: {
215
+ startedAtUtc: string
216
+ endedAtUtc: string
217
+ source: string
218
+ outcome: InstallOutcome
219
+ output?: string
220
+ reason?: string
221
+ }): PackageManagerReport {
222
+ return {
223
+ title: PACKAGE_MANAGER_TITLE,
224
+ headline:
225
+ input.outcome === "succeeded"
226
+ ? "Pi package install completed."
227
+ : "Pi package install failed.",
228
+ tone: input.outcome === "succeeded" ? "success" : "error",
229
+ lines: [
230
+ `Start: ${formatUtcTimestamp(input.startedAtUtc)}`,
231
+ `End: ${formatUtcTimestamp(input.endedAtUtc)}`,
232
+ `Result: ${input.outcome}`,
233
+ `Package source: ${input.source}`,
234
+ ...(input.outcome === "succeeded"
235
+ ? ["Run /reload to activate installed package resources."]
236
+ : []),
237
+ ],
238
+ lineTone: "dim",
239
+ output: input.output ?? input.reason,
240
+ outputLabel:
241
+ input.outcome === "succeeded" ? "Install output:" : "Error detail:",
242
+ outputDescription:
243
+ input.outcome === "succeeded" ? "install output" : "error detail",
244
+ outputTone: "dim",
245
+ hideOutputWhenCollapsed: true,
246
+ }
247
+ }
248
+
249
+ export function createAutomaticUpdateWidgetLines(state: WidgetState): string[] {
250
+ if (state.mode === "status-checking") {
251
+ return ["Pi package status in progress.", "Checking for package updates..."]
252
+ }
253
+
254
+ if (state.mode === "checking") {
255
+ return [
256
+ formatAutomaticUpdateHeadline("in-progress"),
257
+ "Checking for package updates...",
258
+ ]
259
+ }
260
+
261
+ if (state.mode === "installing") {
262
+ return [
263
+ formatAutomaticUpdateHeadline("in-progress"),
264
+ `Installing ${state.packages} package update${state.packages === 1 ? "" : "s"}...`,
265
+ ]
266
+ }
267
+
268
+ if (state.mode === "package-installing") {
269
+ return [
270
+ "Pi package install in progress.",
271
+ `Installing package from ${state.source}...`,
272
+ ]
273
+ }
274
+
275
+ return [
276
+ formatAutomaticUpdateHeadline("succeeded"),
277
+ `Reloading in ${state.secondsRemaining} second${state.secondsRemaining === 1 ? "" : "s"} to activate updated package resources.`,
278
+ ]
279
+ }
280
+
281
+ export function setPackageManagerWidget(
282
+ ctx: ExtensionContext,
283
+ state: WidgetState,
284
+ ): void {
285
+ if (ctx.mode !== "tui") {
286
+ return
287
+ }
288
+
289
+ ctx.ui.setWidget(
290
+ PACKAGE_MANAGER_WIDGET_KEY,
291
+ (_tui, theme) => {
292
+ const background =
293
+ state.mode === "countdown"
294
+ ? (text: string) => theme.bg("toolSuccessBg", text)
295
+ : (text: string) => theme.bg("toolPendingBg", text)
296
+ const box = new Box(1, 1, background)
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
+ },
324
+ { placement: "aboveEditor" },
325
+ )
326
+ }
327
+
328
+ export function clearPackageManagerWidget(ctx: ExtensionContext): void {
329
+ if (ctx.mode !== "tui") {
330
+ return
331
+ }
332
+
333
+ ctx.ui.setWidget(PACKAGE_MANAGER_WIDGET_KEY, undefined)
334
+ }
335
+
336
+ export function getExecDisplayOutput(result: ExecResult): string | undefined {
337
+ const stdout = result.stdout.trim()
338
+ const stderr = result.stderr.trim()
339
+ const sections: string[] = []
340
+
341
+ if (stdout) {
342
+ sections.push(stdout)
343
+ }
344
+
345
+ if (stderr) {
346
+ sections.push(`[stderr]\n${stderr}`)
347
+ }
348
+
349
+ return sections.length > 0 ? sections.join("\n\n") : undefined
350
+ }
351
+
352
+ export function getExecFailureDetail(result: ExecResult): string {
353
+ const stderr = result.stderr.trim()
354
+ const stdout = result.stdout.trim()
355
+ const detail = stderr || stdout || "Package update command failed."
356
+
357
+ return detail.length > 400 ? `${detail.slice(0, 397)}...` : detail
358
+ }
359
+
360
+ function formatAutomaticUpdateHeadline(
361
+ state: AutoUpdateOutcome | "in-progress",
362
+ ): string {
363
+ const suffix =
364
+ state === "in-progress"
365
+ ? "in progress"
366
+ : state === "succeeded"
367
+ ? "completed"
368
+ : state
369
+
370
+ return `Pi package(s) update ${suffix}.`
371
+ }
372
+
373
+ function formatExpandHint(theme: ThemeLike, description: string): string {
374
+ const expandKey = keyText("app.tools.expand") || "Ctrl+O"
375
+
376
+ return `${theme.fg("dim", expandKey)}${theme.fg("muted", ` ${description}`)}`
377
+ }
378
+
379
+ function formatReportOutput(
380
+ output: string,
381
+ expanded: boolean,
382
+ ): { text: string; truncated: boolean } {
383
+ const normalizedLines = output.trim().split(/\r?\n/)
384
+
385
+ if (expanded || normalizedLines.length <= OUTPUT_PREVIEW_LINE_COUNT) {
386
+ return {
387
+ text: normalizedLines.join("\n"),
388
+ truncated: false,
389
+ }
390
+ }
391
+
392
+ return {
393
+ text: normalizedLines.slice(0, OUTPUT_PREVIEW_LINE_COUNT).join("\n"),
394
+ truncated: true,
395
+ }
396
+ }
@@ -0,0 +1,69 @@
1
+ import {
2
+ DefaultPackageManager,
3
+ SettingsManager,
4
+ getAgentDir,
5
+ type ExecResult,
6
+ type ExtensionAPI,
7
+ type ExtensionCommandContext,
8
+ type ExtensionContext,
9
+ } from "@earendil-works/pi-coding-agent"
10
+
11
+ import { INSTALL_COMMAND, UPDATE_COMMAND } from "./model.ts"
12
+
13
+ export interface PackageManagerDeps {
14
+ nowIso(): string
15
+ sleep(milliseconds: number): Promise<void>
16
+ isOffline(): boolean
17
+ checkForAvailableUpdates(ctx: ExtensionContext): Promise<string[]>
18
+ runNativeUpdate(
19
+ pi: Pick<ExtensionAPI, "exec">,
20
+ ctx: ExtensionCommandContext,
21
+ ): Promise<ExecResult>
22
+ runNativeInstall(
23
+ pi: Pick<ExtensionAPI, "exec">,
24
+ ctx: ExtensionCommandContext,
25
+ source: string,
26
+ ): Promise<ExecResult>
27
+ }
28
+
29
+ export const defaultPackageManagerDeps: PackageManagerDeps = {
30
+ nowIso: () => new Date().toISOString(),
31
+ sleep(milliseconds: number) {
32
+ return new Promise((resolve) => {
33
+ setTimeout(resolve, milliseconds)
34
+ })
35
+ },
36
+ isOffline: () => Boolean(process.env.PI_OFFLINE),
37
+ async checkForAvailableUpdates(ctx: ExtensionContext): Promise<string[]> {
38
+ const agentDir = getAgentDir()
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
+ })
47
+ const updates = await packageManager.checkForAvailableUpdates()
48
+
49
+ return updates
50
+ .map((update) => update.displayName)
51
+ .sort((left, right) => left.localeCompare(right))
52
+ },
53
+ runNativeUpdate(pi: Pick<ExtensionAPI, "exec">, ctx: ExtensionCommandContext) {
54
+ return pi.exec("pi", [...UPDATE_COMMAND], {
55
+ cwd: ctx.cwd,
56
+ signal: ctx.signal,
57
+ })
58
+ },
59
+ runNativeInstall(
60
+ pi: Pick<ExtensionAPI, "exec">,
61
+ ctx: ExtensionCommandContext,
62
+ source: string,
63
+ ) {
64
+ return pi.exec("pi", [...INSTALL_COMMAND, source], {
65
+ cwd: ctx.cwd,
66
+ signal: ctx.signal,
67
+ })
68
+ },
69
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaosh3n/pi-package-manager",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Package Manager for Pi",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.23.0",
@@ -27,6 +27,7 @@
27
27
  },
28
28
  "files": [
29
29
  "index.ts",
30
+ "internal/**/*.ts",
30
31
  "README.md",
31
32
  "LICENSE"
32
33
  ],