@gaosh3n/pi-package-manager 0.1.0 → 0.1.1

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,223 @@
1
+ import {
2
+ type ExtensionAPI,
3
+ type ExtensionCommandContext,
4
+ type ExtensionContext,
5
+ type SessionStartEvent,
6
+ } from "@earendil-works/pi-coding-agent"
7
+
8
+ import { RELOAD_COUNTDOWN_SECONDS, REPORT_ENTRY_TYPE } from "./model.ts"
9
+ import {
10
+ appendAutoUpdateRecordAndReport,
11
+ createAutoUpdateRecord,
12
+ getLastAutoUpdateRecord,
13
+ } from "./records.ts"
14
+ import {
15
+ clearPackageManagerWidget,
16
+ createAutoUpdateResultReport,
17
+ createStatusErrorReport,
18
+ createStatusReport,
19
+ getExecDisplayOutput,
20
+ getExecFailureDetail,
21
+ setPackageManagerWidget,
22
+ } from "./reports.ts"
23
+ import { defaultPackageManagerDeps, type PackageManagerDeps } from "./runtime.ts"
24
+
25
+ export function createPackageManagerController(
26
+ pi: Pick<ExtensionAPI, "appendEntry" | "sendUserMessage" | "exec">,
27
+ deps: PackageManagerDeps = defaultPackageManagerDeps,
28
+ ) {
29
+ return {
30
+ onSessionStart,
31
+ handleStatus,
32
+ handleUpdate,
33
+ }
34
+
35
+ async function onSessionStart(
36
+ event: Pick<SessionStartEvent, "reason">,
37
+ _ctx: ExtensionContext,
38
+ ): Promise<void> {
39
+ if (!shouldAutoUpdateOnSessionStart(event)) {
40
+ return
41
+ }
42
+
43
+ pi.sendUserMessage("/package-manager update --startup", {
44
+ expandPromptTemplates: true,
45
+ })
46
+ }
47
+
48
+ async function handleStatus(ctx: ExtensionCommandContext): Promise<void> {
49
+ const lastAutoUpdate = getLastAutoUpdateRecord(ctx.sessionManager.getEntries())
50
+
51
+ setPackageManagerWidget(ctx, { mode: "status-checking" })
52
+
53
+ try {
54
+ const availableUpdates = await deps.checkForAvailableUpdates(ctx)
55
+ pi.appendEntry(
56
+ REPORT_ENTRY_TYPE,
57
+ createStatusReport({
58
+ availableUpdates,
59
+ lastAutoUpdate,
60
+ }),
61
+ )
62
+ } catch (error) {
63
+ pi.appendEntry(
64
+ REPORT_ENTRY_TYPE,
65
+ createStatusErrorReport(
66
+ {
67
+ availableUpdates: [],
68
+ lastAutoUpdate,
69
+ },
70
+ getErrorMessage(error),
71
+ ),
72
+ )
73
+ } finally {
74
+ clearPackageManagerWidget(ctx)
75
+ }
76
+ }
77
+
78
+ async function handleUpdate(
79
+ ctx: ExtensionCommandContext,
80
+ options: { startupTriggered: boolean },
81
+ ): Promise<void> {
82
+ const startedAtUtc = deps.nowIso()
83
+ let shouldClearWidget = true
84
+
85
+ setPackageManagerWidget(ctx, { mode: "checking" })
86
+
87
+ try {
88
+ if (deps.isOffline()) {
89
+ appendSkippedResult(startedAtUtc, "PI_OFFLINE is set.")
90
+ return
91
+ }
92
+
93
+ const availableUpdates = await deps.checkForAvailableUpdates(ctx)
94
+
95
+ if (availableUpdates.length === 0) {
96
+ appendSkippedResult(startedAtUtc, "No package updates are available.")
97
+ return
98
+ }
99
+
100
+ setPackageManagerWidget(ctx, {
101
+ mode: "installing",
102
+ packages: availableUpdates.length,
103
+ })
104
+
105
+ const result = await deps.runNativeUpdate(pi, ctx)
106
+ const output = getExecDisplayOutput(result)
107
+
108
+ if (result.code === 0) {
109
+ const record = createAutoUpdateRecord({
110
+ startedAtUtc,
111
+ endedAtUtc: deps.nowIso(),
112
+ outcome: "succeeded",
113
+ packagesUpdated: availableUpdates.length,
114
+ })
115
+
116
+ appendAutoUpdateRecordAndReport(
117
+ pi,
118
+ record,
119
+ createAutoUpdateResultReport({
120
+ record,
121
+ output,
122
+ reloadAfterSeconds: RELOAD_COUNTDOWN_SECONDS,
123
+ }),
124
+ )
125
+ await runReloadCountdown(ctx)
126
+ clearPackageManagerWidget(ctx)
127
+ shouldClearWidget = false
128
+ await ctx.reload()
129
+ return
130
+ }
131
+
132
+ const record = createAutoUpdateRecord({
133
+ startedAtUtc,
134
+ endedAtUtc: deps.nowIso(),
135
+ outcome: "failed",
136
+ packagesUpdated: 0,
137
+ reason: getExecFailureDetail(result),
138
+ })
139
+
140
+ appendAutoUpdateRecordAndReport(
141
+ pi,
142
+ record,
143
+ createAutoUpdateResultReport({ record, output }),
144
+ )
145
+ notifyStartupFailure(ctx, options.startupTriggered)
146
+ } catch (error) {
147
+ const record = createAutoUpdateRecord({
148
+ startedAtUtc,
149
+ endedAtUtc: deps.nowIso(),
150
+ outcome: "failed",
151
+ packagesUpdated: 0,
152
+ reason: getErrorMessage(error),
153
+ })
154
+
155
+ appendAutoUpdateRecordAndReport(
156
+ pi,
157
+ record,
158
+ createAutoUpdateResultReport({ record }),
159
+ )
160
+ notifyStartupFailure(ctx, options.startupTriggered)
161
+ } finally {
162
+ if (shouldClearWidget) {
163
+ clearPackageManagerWidget(ctx)
164
+ }
165
+ }
166
+ }
167
+
168
+ function appendSkippedResult(startedAtUtc: string, reason: string): void {
169
+ const record = createAutoUpdateRecord({
170
+ startedAtUtc,
171
+ endedAtUtc: deps.nowIso(),
172
+ outcome: "skipped",
173
+ packagesUpdated: 0,
174
+ reason,
175
+ })
176
+
177
+ appendAutoUpdateRecordAndReport(
178
+ pi,
179
+ record,
180
+ createAutoUpdateResultReport({ record }),
181
+ )
182
+ }
183
+
184
+ async function runReloadCountdown(
185
+ ctx: ExtensionContext,
186
+ seconds = RELOAD_COUNTDOWN_SECONDS,
187
+ ): Promise<void> {
188
+ for (let remaining = seconds; remaining >= 1; remaining--) {
189
+ setPackageManagerWidget(ctx, {
190
+ mode: "countdown",
191
+ secondsRemaining: remaining,
192
+ })
193
+
194
+ await deps.sleep(1000)
195
+ }
196
+ }
197
+ }
198
+
199
+ export function shouldAutoUpdateOnSessionStart(
200
+ event: Pick<SessionStartEvent, "reason">,
201
+ ): boolean {
202
+ return event.reason === "startup"
203
+ }
204
+
205
+ function notifyStartupFailure(
206
+ ctx: ExtensionCommandContext,
207
+ startupTriggered: boolean,
208
+ ): void {
209
+ if (startupTriggered && ctx.hasUI) {
210
+ ctx.ui.notify(
211
+ "Pi Package Manager automatic startup update failed. See transcript for details.",
212
+ "error",
213
+ )
214
+ }
215
+ }
216
+
217
+ function getErrorMessage(error: unknown): string {
218
+ if (error instanceof Error) {
219
+ return error.message
220
+ }
221
+
222
+ return String(error)
223
+ }
@@ -0,0 +1,41 @@
1
+ export const AUTO_UPDATE_RECORD_ENTRY_TYPE = "package-manager-auto-update-record"
2
+ export const REPORT_ENTRY_TYPE = "package-manager-report"
3
+ export const PACKAGE_MANAGER_TITLE = "Pi Package Manager"
4
+ export const PACKAGE_MANAGER_WIDGET_KEY = "pi-package-manager"
5
+ export const RELOAD_COUNTDOWN_SECONDS = 5
6
+ export const OUTPUT_PREVIEW_LINE_COUNT = 8
7
+ export const UPDATE_COMMAND = ["update", "--extensions"] as const
8
+
9
+ export type AutoUpdateOutcome = "succeeded" | "failed" | "skipped"
10
+ export type ReportTone = "info" | "success" | "warning" | "error"
11
+
12
+ export type WidgetState =
13
+ | { mode: "status-checking" }
14
+ | { mode: "checking" }
15
+ | { mode: "installing"; packages: number }
16
+ | { mode: "countdown"; secondsRemaining: number }
17
+
18
+ export interface AutoUpdateRecord {
19
+ startedAtUtc: string
20
+ endedAtUtc: string
21
+ outcome: AutoUpdateOutcome
22
+ packagesUpdated: number
23
+ reason?: string
24
+ }
25
+
26
+ export interface PackageManagerReport {
27
+ title: string
28
+ headline?: string
29
+ tone: ReportTone
30
+ lines: string[]
31
+ lineTone?: "default" | "dim"
32
+ output?: string
33
+ outputLabel?: string
34
+ outputTone?: "default" | "dim"
35
+ hideOutputWhenCollapsed?: boolean
36
+ }
37
+
38
+ export interface PackageStatusSnapshot {
39
+ availableUpdates: string[]
40
+ lastAutoUpdate?: AutoUpdateRecord
41
+ }
@@ -0,0 +1,81 @@
1
+ import {
2
+ type CustomEntry,
3
+ type ExtensionAPI,
4
+ type SessionEntry,
5
+ } from "@earendil-works/pi-coding-agent"
6
+
7
+ import {
8
+ AUTO_UPDATE_RECORD_ENTRY_TYPE,
9
+ REPORT_ENTRY_TYPE,
10
+ type AutoUpdateOutcome,
11
+ type AutoUpdateRecord,
12
+ type PackageManagerReport,
13
+ } from "./model.ts"
14
+
15
+ export function createAutoUpdateRecord(input: {
16
+ startedAtUtc: string
17
+ endedAtUtc: string
18
+ outcome: AutoUpdateOutcome
19
+ packagesUpdated: number
20
+ reason?: string
21
+ }): AutoUpdateRecord {
22
+ return {
23
+ startedAtUtc: input.startedAtUtc,
24
+ endedAtUtc: input.endedAtUtc,
25
+ outcome: input.outcome,
26
+ packagesUpdated: input.packagesUpdated,
27
+ reason: input.reason,
28
+ }
29
+ }
30
+
31
+ export function appendAutoUpdateRecordAndReport(
32
+ pi: Pick<ExtensionAPI, "appendEntry">,
33
+ record: AutoUpdateRecord,
34
+ report: PackageManagerReport,
35
+ ): void {
36
+ pi.appendEntry(AUTO_UPDATE_RECORD_ENTRY_TYPE, record)
37
+ pi.appendEntry(REPORT_ENTRY_TYPE, report)
38
+ }
39
+
40
+ export function getLastAutoUpdateRecord(
41
+ entries: readonly SessionEntry[],
42
+ ): AutoUpdateRecord | undefined {
43
+ for (let index = entries.length - 1; index >= 0; index--) {
44
+ const entry = entries[index]
45
+
46
+ if (!isAutoUpdateRecordEntry(entry)) {
47
+ continue
48
+ }
49
+
50
+ return entry.data
51
+ }
52
+
53
+ return undefined
54
+ }
55
+
56
+ export function isAutoUpdateRecordEntry(
57
+ entry: SessionEntry,
58
+ ): entry is CustomEntry<AutoUpdateRecord> {
59
+ return (
60
+ entry.type === "custom" &&
61
+ entry.customType === AUTO_UPDATE_RECORD_ENTRY_TYPE &&
62
+ isAutoUpdateRecord(entry.data)
63
+ )
64
+ }
65
+
66
+ export function isAutoUpdateRecord(value: unknown): value is AutoUpdateRecord {
67
+ if (!value || typeof value !== "object") {
68
+ return false
69
+ }
70
+
71
+ const candidate = value as Partial<AutoUpdateRecord>
72
+ return (
73
+ typeof candidate.startedAtUtc === "string" &&
74
+ typeof candidate.endedAtUtc === "string" &&
75
+ (candidate.outcome === "succeeded" ||
76
+ candidate.outcome === "failed" ||
77
+ candidate.outcome === "skipped") &&
78
+ typeof candidate.packagesUpdated === "number" &&
79
+ (candidate.reason === undefined || typeof candidate.reason === "string")
80
+ )
81
+ }
@@ -0,0 +1,351 @@
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 PackageManagerReport,
15
+ type PackageStatusSnapshot,
16
+ type WidgetState,
17
+ } from "./model.ts"
18
+
19
+ interface ThemeLike {
20
+ fg(token: string, text: string): string
21
+ bg(token: string, text: string): string
22
+ bold(text: string): string
23
+ }
24
+
25
+ export function createReportEntryRenderer() {
26
+ return (
27
+ entry: { data?: PackageManagerReport },
28
+ { expanded }: { expanded: boolean },
29
+ theme: ThemeLike,
30
+ ) => {
31
+ const report = entry.data
32
+
33
+ if (!report) {
34
+ return undefined
35
+ }
36
+
37
+ const toneColor =
38
+ report.tone === "error"
39
+ ? "error"
40
+ : report.tone === "warning"
41
+ ? "warning"
42
+ : report.tone === "success"
43
+ ? "success"
44
+ : "accent"
45
+ const lineTextTone = report.lineTone === "dim" ? "dim" : "customMessageText"
46
+ const outputTextTone = report.outputTone === "dim" ? "dim" : lineTextTone
47
+
48
+ const box = new Box(1, 1, (text: string) => theme.bg("customMessageBg", text))
49
+
50
+ box.addChild(
51
+ new Text(
52
+ `${theme.fg(toneColor, "●")} ${theme.bold(theme.fg("customMessageLabel", report.title))}`,
53
+ 0,
54
+ 0,
55
+ ),
56
+ )
57
+
58
+ if (report.headline) {
59
+ box.addChild(new Text(theme.fg("text", report.headline), 0, 0))
60
+ }
61
+
62
+ if (report.lines.length > 0) {
63
+ box.addChild(
64
+ new Text(
65
+ report.lines.map((line) => theme.fg(lineTextTone, line)).join("\n"),
66
+ 0,
67
+ 0,
68
+ ),
69
+ )
70
+ }
71
+
72
+ if (report.output?.trim()) {
73
+ if (!expanded && report.hideOutputWhenCollapsed) {
74
+ box.addChild(
75
+ new Text(
76
+ formatExpandHint(theme, "to expand to see update output."),
77
+ 0,
78
+ 0,
79
+ ),
80
+ )
81
+ } else {
82
+ const { text, truncated } = formatReportOutput(report.output, expanded)
83
+
84
+ box.addChild(
85
+ new Text(
86
+ theme.fg(
87
+ outputTextTone,
88
+ report.outputLabel ?? "Update output:",
89
+ ),
90
+ 0,
91
+ 0,
92
+ ),
93
+ )
94
+ box.addChild(new Text(theme.fg(outputTextTone, text), 0, 0))
95
+
96
+ if (truncated) {
97
+ box.addChild(
98
+ new Text(
99
+ formatExpandHint(
100
+ theme,
101
+ "to expand to view the full update output.",
102
+ ),
103
+ 0,
104
+ 0,
105
+ ),
106
+ )
107
+ }
108
+ }
109
+ }
110
+
111
+ return box
112
+ }
113
+ }
114
+
115
+ export function formatUtcTimestamp(isoUtc: string): string {
116
+ return isoUtc.replace("T", " ").replace(/\.\d{3}Z$/, " UTC+00")
117
+ }
118
+
119
+ export function formatStatusLines(snapshot: PackageStatusSnapshot): string[] {
120
+ if (!snapshot.lastAutoUpdate) {
121
+ return ["Latest package update: none recorded."]
122
+ }
123
+
124
+ const lines = [
125
+ `Latest update start: ${formatUtcTimestamp(snapshot.lastAutoUpdate.startedAtUtc)}`,
126
+ `Latest update end: ${formatUtcTimestamp(snapshot.lastAutoUpdate.endedAtUtc)}`,
127
+ `Latest update result: ${snapshot.lastAutoUpdate.outcome}`,
128
+ `Latest update packages updated: ${snapshot.lastAutoUpdate.packagesUpdated}`,
129
+ ]
130
+
131
+ if (snapshot.lastAutoUpdate.reason) {
132
+ lines.push(`Latest update detail: ${snapshot.lastAutoUpdate.reason}`)
133
+ }
134
+
135
+ return lines
136
+ }
137
+
138
+ export function createStatusReport(
139
+ snapshot: PackageStatusSnapshot,
140
+ ): PackageManagerReport {
141
+ return {
142
+ title: PACKAGE_MANAGER_TITLE,
143
+ headline:
144
+ snapshot.availableUpdates.length === 0
145
+ ? "No package updates are available."
146
+ : `${snapshot.availableUpdates.length} package update${snapshot.availableUpdates.length === 1 ? " is" : "s are"} available.`,
147
+ tone: snapshot.availableUpdates.length > 0 ? "warning" : "info",
148
+ lines: formatStatusLines(snapshot),
149
+ lineTone: "dim",
150
+ output:
151
+ snapshot.availableUpdates.length > 0
152
+ ? snapshot.availableUpdates.map((name) => `- ${name}`).join("\n")
153
+ : undefined,
154
+ outputLabel:
155
+ snapshot.availableUpdates.length > 0 ? "Available updates:" : undefined,
156
+ outputTone: "dim",
157
+ }
158
+ }
159
+
160
+ export function createStatusErrorReport(
161
+ snapshot: PackageStatusSnapshot,
162
+ errorMessage: string,
163
+ ): PackageManagerReport {
164
+ return {
165
+ title: PACKAGE_MANAGER_TITLE,
166
+ headline: "Package status check failed.",
167
+ tone: "error",
168
+ lines: formatStatusLines(snapshot),
169
+ lineTone: "dim",
170
+ output: `Failed to check package updates: ${errorMessage}`,
171
+ outputLabel: "Error detail:",
172
+ outputTone: "dim",
173
+ }
174
+ }
175
+
176
+ export function createAutoUpdateResultReport(input: {
177
+ record: AutoUpdateRecord
178
+ output?: string
179
+ reloadAfterSeconds?: number
180
+ }): PackageManagerReport {
181
+ const lines = [
182
+ `Start: ${formatUtcTimestamp(input.record.startedAtUtc)}`,
183
+ `End: ${formatUtcTimestamp(input.record.endedAtUtc)}`,
184
+ `Result: ${input.record.outcome}`,
185
+ `Packages updated: ${input.record.packagesUpdated}`,
186
+ ]
187
+
188
+ if (input.reloadAfterSeconds) {
189
+ lines.push(
190
+ `Reloading in ${input.reloadAfterSeconds} seconds to activate updated package resources.`,
191
+ )
192
+ }
193
+
194
+ return {
195
+ title: PACKAGE_MANAGER_TITLE,
196
+ headline: formatAutomaticUpdateHeadline(input.record.outcome),
197
+ tone:
198
+ input.record.outcome === "failed"
199
+ ? "error"
200
+ : input.record.outcome === "succeeded"
201
+ ? "success"
202
+ : "info",
203
+ lines,
204
+ lineTone: "dim",
205
+ output: input.output ?? input.record.reason,
206
+ outputTone: "dim",
207
+ hideOutputWhenCollapsed: true,
208
+ }
209
+ }
210
+
211
+ export function createAutomaticUpdateWidgetLines(state: WidgetState): string[] {
212
+ if (state.mode === "status-checking") {
213
+ return ["Pi package status in progress.", "Checking for package updates..."]
214
+ }
215
+
216
+ if (state.mode === "checking") {
217
+ return [
218
+ formatAutomaticUpdateHeadline("in-progress"),
219
+ "Checking for package updates...",
220
+ ]
221
+ }
222
+
223
+ if (state.mode === "installing") {
224
+ return [
225
+ formatAutomaticUpdateHeadline("in-progress"),
226
+ `Installing ${state.packages} package update${state.packages === 1 ? "" : "s"}...`,
227
+ ]
228
+ }
229
+
230
+ return [
231
+ formatAutomaticUpdateHeadline("succeeded"),
232
+ `Reloading in ${state.secondsRemaining} second${state.secondsRemaining === 1 ? "" : "s"} to activate updated package resources.`,
233
+ ]
234
+ }
235
+
236
+ export function setPackageManagerWidget(
237
+ ctx: ExtensionContext,
238
+ state: WidgetState,
239
+ ): void {
240
+ if (ctx.mode !== "tui") {
241
+ return
242
+ }
243
+
244
+ ctx.ui.setWidget(
245
+ PACKAGE_MANAGER_WIDGET_KEY,
246
+ (_tui, theme) => {
247
+ const background =
248
+ state.mode === "countdown"
249
+ ? (text: string) => theme.bg("toolSuccessBg", text)
250
+ : (text: string) => theme.bg("toolPendingBg", text)
251
+ const box = new Box(1, 1, background)
252
+ const titleColor = state.mode === "countdown" ? "success" : "accent"
253
+ const bodyLines = createAutomaticUpdateWidgetLines(state)
254
+
255
+ box.addChild(
256
+ new Text(
257
+ `${theme.fg(titleColor, "●")} ${theme.bold(theme.fg("customMessageLabel", PACKAGE_MANAGER_TITLE))}`,
258
+ 0,
259
+ 0,
260
+ ),
261
+ )
262
+ box.addChild(
263
+ new Text(
264
+ bodyLines
265
+ .map((line, index) =>
266
+ theme.fg(
267
+ index === bodyLines.length - 1 ? "dim" : "text",
268
+ line,
269
+ ),
270
+ )
271
+ .join("\n"),
272
+ 0,
273
+ 0,
274
+ ),
275
+ )
276
+
277
+ return box
278
+ },
279
+ { placement: "aboveEditor" },
280
+ )
281
+ }
282
+
283
+ export function clearPackageManagerWidget(ctx: ExtensionContext): void {
284
+ if (ctx.mode !== "tui") {
285
+ return
286
+ }
287
+
288
+ ctx.ui.setWidget(PACKAGE_MANAGER_WIDGET_KEY, undefined)
289
+ }
290
+
291
+ export function getExecDisplayOutput(result: ExecResult): string | undefined {
292
+ const stdout = result.stdout.trim()
293
+ const stderr = result.stderr.trim()
294
+ const sections: string[] = []
295
+
296
+ if (stdout) {
297
+ sections.push(stdout)
298
+ }
299
+
300
+ if (stderr) {
301
+ sections.push(`[stderr]\n${stderr}`)
302
+ }
303
+
304
+ return sections.length > 0 ? sections.join("\n\n") : undefined
305
+ }
306
+
307
+ export function getExecFailureDetail(result: ExecResult): string {
308
+ const stderr = result.stderr.trim()
309
+ const stdout = result.stdout.trim()
310
+ const detail = stderr || stdout || "Package update command failed."
311
+
312
+ return detail.length > 400 ? `${detail.slice(0, 397)}...` : detail
313
+ }
314
+
315
+ function formatAutomaticUpdateHeadline(
316
+ state: AutoUpdateOutcome | "in-progress",
317
+ ): string {
318
+ const suffix =
319
+ state === "in-progress"
320
+ ? "in progress"
321
+ : state === "succeeded"
322
+ ? "completed"
323
+ : state
324
+
325
+ return `Pi package(s) update ${suffix}.`
326
+ }
327
+
328
+ function formatExpandHint(theme: ThemeLike, description: string): string {
329
+ const expandKey = keyText("app.tools.expand") || "Ctrl+O"
330
+
331
+ return `${theme.fg("dim", expandKey)}${theme.fg("muted", ` ${description}`)}`
332
+ }
333
+
334
+ function formatReportOutput(
335
+ output: string,
336
+ expanded: boolean,
337
+ ): { text: string; truncated: boolean } {
338
+ const normalizedLines = output.trim().split(/\r?\n/)
339
+
340
+ if (expanded || normalizedLines.length <= OUTPUT_PREVIEW_LINE_COUNT) {
341
+ return {
342
+ text: normalizedLines.join("\n"),
343
+ truncated: false,
344
+ }
345
+ }
346
+
347
+ return {
348
+ text: normalizedLines.slice(0, OUTPUT_PREVIEW_LINE_COUNT).join("\n"),
349
+ truncated: true,
350
+ }
351
+ }