opencode-manager 0.3.1 → 0.4.0

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,661 @@
1
+ /**
2
+ * CLI output module.
3
+ *
4
+ * Provides a unified interface for outputting data in different formats
5
+ * based on the --format global option (json, ndjson, table).
6
+ *
7
+ * This module acts as a router that selects the appropriate formatter
8
+ * based on the format option and handles all domain-specific output types.
9
+ */
10
+
11
+ import type { GlobalOptions } from "./index"
12
+ import type {
13
+ AggregateTokenSummary,
14
+ ChatMessage,
15
+ ChatSearchResult,
16
+ ProjectRecord,
17
+ SessionRecord,
18
+ TokenSummary,
19
+ } from "../lib/opencode-data"
20
+
21
+ // Import formatters
22
+ import {
23
+ formatJson,
24
+ formatJsonArraySuccess,
25
+ formatJsonError,
26
+ formatJsonSuccess,
27
+ printJson,
28
+ printJsonArraySuccess,
29
+ printJsonError,
30
+ printJsonSuccess,
31
+ type JsonFormatOptions,
32
+ type JsonResponse,
33
+ } from "./formatters/json"
34
+ import { formatNdjson, printNdjson } from "./formatters/ndjson"
35
+ import {
36
+ formatAggregateTokenSummary,
37
+ formatChatSearchTable,
38
+ formatChatTable,
39
+ formatProjectsTable,
40
+ formatSessionsTable,
41
+ formatTokenSummary,
42
+ printAggregateTokenSummary,
43
+ printChatSearchTable,
44
+ printChatTable,
45
+ printProjectsTable,
46
+ printSessionsTable,
47
+ printTokenSummary,
48
+ type IndexedChatSearchResult,
49
+ type TableFormatOptions,
50
+ } from "./formatters/table"
51
+
52
+ // ========================
53
+ // Types
54
+ // ========================
55
+
56
+ /**
57
+ * Output format types supported by the CLI.
58
+ */
59
+ export type OutputFormat = "json" | "ndjson" | "table"
60
+
61
+ /**
62
+ * Output options derived from global CLI options.
63
+ */
64
+ export interface OutputOptions {
65
+ format: OutputFormat
66
+ quiet?: boolean
67
+ /** Metadata for list responses (limit, truncation info) */
68
+ meta?: {
69
+ limit?: number
70
+ truncated?: boolean
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Extract output options from global CLI options.
76
+ */
77
+ export function getOutputOptions(globalOpts: GlobalOptions): OutputOptions {
78
+ return {
79
+ format: globalOpts.format,
80
+ quiet: globalOpts.quiet,
81
+ meta: {
82
+ limit: globalOpts.limit,
83
+ },
84
+ }
85
+ }
86
+
87
+ // ========================
88
+ // Generic Output Functions
89
+ // ========================
90
+
91
+ /**
92
+ * Format any data array using the specified output format.
93
+ * This is the low-level generic formatter - prefer domain-specific functions.
94
+ */
95
+ export function formatOutput<T>(
96
+ data: T[],
97
+ format: OutputFormat,
98
+ tableFormatter?: (data: T[], options?: TableFormatOptions) => string,
99
+ options?: OutputOptions
100
+ ): string {
101
+ switch (format) {
102
+ case "json":
103
+ return formatJsonArraySuccess(data, options?.meta, {
104
+ pretty: process.stdout.isTTY,
105
+ })
106
+ case "ndjson":
107
+ return formatNdjson(data)
108
+ case "table":
109
+ if (tableFormatter) {
110
+ return tableFormatter(data)
111
+ }
112
+ // Fallback: format as JSON if no table formatter provided
113
+ return formatJsonArraySuccess(data, options?.meta)
114
+ default:
115
+ // Exhaustive check
116
+ const _exhaustive: never = format
117
+ throw new Error(`Unknown format: ${_exhaustive}`)
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Print any data array using the specified output format.
123
+ */
124
+ export function printOutput<T>(
125
+ data: T[],
126
+ format: OutputFormat,
127
+ tableFormatter?: (data: T[], options?: TableFormatOptions) => string,
128
+ options?: OutputOptions
129
+ ): void {
130
+ console.log(formatOutput(data, format, tableFormatter, options))
131
+ }
132
+
133
+ /**
134
+ * Format a single item using the specified output format.
135
+ */
136
+ export function formatSingleOutput<T>(
137
+ data: T,
138
+ format: OutputFormat,
139
+ tableFormatter?: (data: T, options?: TableFormatOptions) => string
140
+ ): string {
141
+ switch (format) {
142
+ case "json":
143
+ return formatJsonSuccess(data, undefined, {
144
+ pretty: process.stdout.isTTY,
145
+ })
146
+ case "ndjson":
147
+ return formatNdjson([data])
148
+ case "table":
149
+ if (tableFormatter) {
150
+ return tableFormatter(data)
151
+ }
152
+ return formatJsonSuccess(data)
153
+ default:
154
+ const _exhaustive: never = format
155
+ throw new Error(`Unknown format: ${_exhaustive}`)
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Print a single item using the specified output format.
161
+ */
162
+ export function printSingleOutput<T>(
163
+ data: T,
164
+ format: OutputFormat,
165
+ tableFormatter?: (data: T, options?: TableFormatOptions) => string
166
+ ): void {
167
+ console.log(formatSingleOutput(data, format, tableFormatter))
168
+ }
169
+
170
+ // ========================
171
+ // Projects Output
172
+ // ========================
173
+
174
+ /**
175
+ * Format projects list for output.
176
+ */
177
+ export function formatProjectsOutput(
178
+ projects: ProjectRecord[],
179
+ options: OutputOptions
180
+ ): string {
181
+ switch (options.format) {
182
+ case "json":
183
+ return formatJsonArraySuccess(projects, options.meta, {
184
+ pretty: process.stdout.isTTY,
185
+ })
186
+ case "ndjson":
187
+ return formatNdjson(projects)
188
+ case "table":
189
+ return formatProjectsTable(projects)
190
+ default:
191
+ const _exhaustive: never = options.format
192
+ throw new Error(`Unknown format: ${_exhaustive}`)
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Print projects list to stdout.
198
+ */
199
+ export function printProjectsOutput(
200
+ projects: ProjectRecord[],
201
+ options: OutputOptions
202
+ ): void {
203
+ if (options.quiet && options.format === "table") {
204
+ // In quiet mode with table format, just show count
205
+ console.log(`${projects.length} project(s)`)
206
+ return
207
+ }
208
+ console.log(formatProjectsOutput(projects, options))
209
+ }
210
+
211
+ // ========================
212
+ // Sessions Output
213
+ // ========================
214
+
215
+ /**
216
+ * Format sessions list for output.
217
+ */
218
+ export function formatSessionsOutput(
219
+ sessions: SessionRecord[],
220
+ options: OutputOptions
221
+ ): string {
222
+ switch (options.format) {
223
+ case "json":
224
+ return formatJsonArraySuccess(sessions, options.meta, {
225
+ pretty: process.stdout.isTTY,
226
+ })
227
+ case "ndjson":
228
+ return formatNdjson(sessions)
229
+ case "table":
230
+ return formatSessionsTable(sessions)
231
+ default:
232
+ const _exhaustive: never = options.format
233
+ throw new Error(`Unknown format: ${_exhaustive}`)
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Print sessions list to stdout.
239
+ */
240
+ export function printSessionsOutput(
241
+ sessions: SessionRecord[],
242
+ options: OutputOptions
243
+ ): void {
244
+ if (options.quiet && options.format === "table") {
245
+ console.log(`${sessions.length} session(s)`)
246
+ return
247
+ }
248
+ console.log(formatSessionsOutput(sessions, options))
249
+ }
250
+
251
+ // ========================
252
+ // Chat Output
253
+ // ========================
254
+
255
+ /**
256
+ * Chat message with index for list display.
257
+ */
258
+ export type IndexedChatMessage = ChatMessage & { index: number }
259
+
260
+ /**
261
+ * Format chat messages list for output.
262
+ */
263
+ export function formatChatOutput(
264
+ messages: IndexedChatMessage[],
265
+ options: OutputOptions
266
+ ): string {
267
+ switch (options.format) {
268
+ case "json":
269
+ return formatJsonArraySuccess(messages, options.meta, {
270
+ pretty: process.stdout.isTTY,
271
+ })
272
+ case "ndjson":
273
+ return formatNdjson(messages)
274
+ case "table":
275
+ return formatChatTable(messages)
276
+ default:
277
+ const _exhaustive: never = options.format
278
+ throw new Error(`Unknown format: ${_exhaustive}`)
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Print chat messages list to stdout.
284
+ */
285
+ export function printChatOutput(
286
+ messages: IndexedChatMessage[],
287
+ options: OutputOptions
288
+ ): void {
289
+ if (options.quiet && options.format === "table") {
290
+ console.log(`${messages.length} message(s)`)
291
+ return
292
+ }
293
+ console.log(formatChatOutput(messages, options))
294
+ }
295
+
296
+ /**
297
+ * Format a single chat message for detailed display.
298
+ * For table format, shows the full message content rather than a table row.
299
+ */
300
+ export function formatChatMessageOutput(
301
+ message: ChatMessage,
302
+ format: OutputFormat
303
+ ): string {
304
+ switch (format) {
305
+ case "json":
306
+ return formatJsonSuccess(message, undefined, {
307
+ pretty: process.stdout.isTTY,
308
+ })
309
+ case "ndjson":
310
+ return formatNdjson([message])
311
+ case "table":
312
+ // For single message display, show formatted content
313
+ const lines: string[] = []
314
+ lines.push(`Message ID: ${message.messageId}`)
315
+ lines.push(`Role: ${message.role}`)
316
+ lines.push(`Created: ${message.createdAt?.toISOString() ?? "unknown"}`)
317
+ if (message.tokens) {
318
+ lines.push(`Tokens: ${message.tokens.total}`)
319
+ }
320
+ lines.push("")
321
+ lines.push("Content:")
322
+ lines.push("-".repeat(40))
323
+ lines.push(message.previewText ?? "[No content]")
324
+ return lines.join("\n")
325
+ default:
326
+ const _exhaustive: never = format
327
+ throw new Error(`Unknown format: ${_exhaustive}`)
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Print a single chat message to stdout.
333
+ */
334
+ export function printChatMessageOutput(
335
+ message: ChatMessage,
336
+ format: OutputFormat
337
+ ): void {
338
+ console.log(formatChatMessageOutput(message, format))
339
+ }
340
+
341
+ // ========================
342
+ // Chat Search Output
343
+ // ========================
344
+
345
+ /**
346
+ * Format chat search results for output.
347
+ */
348
+ export function formatChatSearchOutput(
349
+ results: IndexedChatSearchResult[],
350
+ options: OutputOptions
351
+ ): string {
352
+ switch (options.format) {
353
+ case "json":
354
+ return formatJsonArraySuccess(results, options.meta, {
355
+ pretty: process.stdout.isTTY,
356
+ })
357
+ case "ndjson":
358
+ return formatNdjson(results)
359
+ case "table":
360
+ return formatChatSearchTable(results)
361
+ default:
362
+ const _exhaustive: never = options.format
363
+ throw new Error(`Unknown format: ${_exhaustive}`)
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Print chat search results to stdout.
369
+ */
370
+ export function printChatSearchOutput(
371
+ results: IndexedChatSearchResult[],
372
+ options: OutputOptions
373
+ ): void {
374
+ if (options.quiet && options.format === "table") {
375
+ console.log(`${results.length} match(es)`)
376
+ return
377
+ }
378
+ console.log(formatChatSearchOutput(results, options))
379
+ }
380
+
381
+ // ========================
382
+ // Tokens Output
383
+ // ========================
384
+
385
+ /**
386
+ * Format token summary for output.
387
+ */
388
+ export function formatTokensOutput(
389
+ summary: TokenSummary,
390
+ format: OutputFormat
391
+ ): string {
392
+ switch (format) {
393
+ case "json":
394
+ return formatJsonSuccess(summary, undefined, {
395
+ pretty: process.stdout.isTTY,
396
+ })
397
+ case "ndjson":
398
+ return formatNdjson([summary])
399
+ case "table":
400
+ return formatTokenSummary(summary)
401
+ default:
402
+ const _exhaustive: never = format
403
+ throw new Error(`Unknown format: ${_exhaustive}`)
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Print token summary to stdout.
409
+ */
410
+ export function printTokensOutput(
411
+ summary: TokenSummary,
412
+ format: OutputFormat
413
+ ): void {
414
+ console.log(formatTokensOutput(summary, format))
415
+ }
416
+
417
+ /**
418
+ * Format aggregate token summary for output.
419
+ */
420
+ export function formatAggregateTokensOutput(
421
+ summary: AggregateTokenSummary,
422
+ format: OutputFormat,
423
+ label?: string
424
+ ): string {
425
+ switch (format) {
426
+ case "json":
427
+ return formatJsonSuccess(summary, undefined, {
428
+ pretty: process.stdout.isTTY,
429
+ })
430
+ case "ndjson":
431
+ return formatNdjson([summary])
432
+ case "table":
433
+ return formatAggregateTokenSummary(summary, { label })
434
+ default:
435
+ const _exhaustive: never = format
436
+ throw new Error(`Unknown format: ${_exhaustive}`)
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Print aggregate token summary to stdout.
442
+ */
443
+ export function printAggregateTokensOutput(
444
+ summary: AggregateTokenSummary,
445
+ format: OutputFormat,
446
+ label?: string
447
+ ): void {
448
+ console.log(formatAggregateTokensOutput(summary, format, label))
449
+ }
450
+
451
+ // ========================
452
+ // Error Output
453
+ // ========================
454
+
455
+ /**
456
+ * Format an error for output.
457
+ */
458
+ export function formatErrorOutput(
459
+ error: string | Error,
460
+ format: OutputFormat
461
+ ): string {
462
+ switch (format) {
463
+ case "json":
464
+ case "ndjson":
465
+ return formatJsonError(error, { pretty: process.stdout.isTTY })
466
+ case "table":
467
+ // For table format, just return the error message
468
+ const message = error instanceof Error ? error.message : error
469
+ return `Error: ${message}`
470
+ default:
471
+ const _exhaustive: never = format
472
+ throw new Error(`Unknown format: ${_exhaustive}`)
473
+ }
474
+ }
475
+
476
+ /**
477
+ * Print an error to stderr.
478
+ */
479
+ export function printErrorOutput(
480
+ error: string | Error,
481
+ format: OutputFormat
482
+ ): void {
483
+ console.error(formatErrorOutput(error, format))
484
+ }
485
+
486
+ // ========================
487
+ // Success/Info Output
488
+ // ========================
489
+
490
+ /**
491
+ * Format a success message for output.
492
+ */
493
+ export function formatSuccessOutput(
494
+ message: string,
495
+ data?: Record<string, unknown>,
496
+ format: OutputFormat = "table"
497
+ ): string {
498
+ switch (format) {
499
+ case "json":
500
+ case "ndjson":
501
+ return formatJsonSuccess(
502
+ data ?? { message },
503
+ undefined,
504
+ { pretty: process.stdout.isTTY }
505
+ )
506
+ case "table":
507
+ return message
508
+ default:
509
+ const _exhaustive: never = format
510
+ throw new Error(`Unknown format: ${_exhaustive}`)
511
+ }
512
+ }
513
+
514
+ /**
515
+ * Print a success message to stdout.
516
+ */
517
+ export function printSuccessOutput(
518
+ message: string,
519
+ data?: Record<string, unknown>,
520
+ format: OutputFormat = "table"
521
+ ): void {
522
+ console.log(formatSuccessOutput(message, data, format))
523
+ }
524
+
525
+ // ========================
526
+ // Dry-Run Output
527
+ // ========================
528
+
529
+ /**
530
+ * Dry-run result for delete operations.
531
+ * Mirrors DeleteResult from opencode-data but typed for CLI output.
532
+ */
533
+ export interface DryRunResult {
534
+ /** Paths that would be affected */
535
+ paths: string[]
536
+ /** Operation that would be performed */
537
+ operation: "delete" | "backup" | "move" | "copy"
538
+ /** Resource type (project, session) */
539
+ resourceType: "project" | "session"
540
+ /** Count of items affected */
541
+ count: number
542
+ }
543
+
544
+ /**
545
+ * Format dry-run output showing what would be affected.
546
+ *
547
+ * @param result - The dry-run result
548
+ * @param format - Output format
549
+ * @returns Formatted string
550
+ */
551
+ export function formatDryRunOutput(
552
+ result: DryRunResult,
553
+ format: OutputFormat
554
+ ): string {
555
+ switch (format) {
556
+ case "json":
557
+ return formatJsonSuccess(
558
+ {
559
+ dryRun: true,
560
+ operation: result.operation,
561
+ resourceType: result.resourceType,
562
+ count: result.count,
563
+ paths: result.paths,
564
+ },
565
+ undefined,
566
+ { pretty: process.stdout.isTTY }
567
+ )
568
+ case "ndjson":
569
+ return formatNdjson(
570
+ result.paths.map((path) => ({
571
+ dryRun: true,
572
+ operation: result.operation,
573
+ resourceType: result.resourceType,
574
+ path,
575
+ }))
576
+ )
577
+ case "table": {
578
+ const lines: string[] = []
579
+ lines.push(`[DRY RUN] Would ${result.operation} ${result.count} ${result.resourceType}(s):`)
580
+ lines.push("")
581
+ for (const path of result.paths) {
582
+ lines.push(` ${path}`)
583
+ }
584
+ return lines.join("\n")
585
+ }
586
+ default:
587
+ const _exhaustive: never = format
588
+ throw new Error(`Unknown format: ${_exhaustive}`)
589
+ }
590
+ }
591
+
592
+ /**
593
+ * Print dry-run output to stdout.
594
+ *
595
+ * @param result - The dry-run result
596
+ * @param format - Output format
597
+ */
598
+ export function printDryRunOutput(
599
+ result: DryRunResult,
600
+ format: OutputFormat
601
+ ): void {
602
+ console.log(formatDryRunOutput(result, format))
603
+ }
604
+
605
+ /**
606
+ * Create a DryRunResult from a list of paths.
607
+ *
608
+ * @param paths - List of file paths
609
+ * @param operation - The operation being performed
610
+ * @param resourceType - The type of resource
611
+ * @returns DryRunResult object
612
+ */
613
+ export function createDryRunResult(
614
+ paths: string[],
615
+ operation: DryRunResult["operation"],
616
+ resourceType: DryRunResult["resourceType"]
617
+ ): DryRunResult {
618
+ return {
619
+ paths,
620
+ operation,
621
+ resourceType,
622
+ count: paths.length,
623
+ }
624
+ }
625
+
626
+ // ========================
627
+ // Re-exports for convenience
628
+ // ========================
629
+
630
+ export {
631
+ // JSON formatter exports
632
+ formatJson,
633
+ formatJsonArraySuccess,
634
+ formatJsonError,
635
+ formatJsonSuccess,
636
+ printJson,
637
+ printJsonArraySuccess,
638
+ printJsonError,
639
+ printJsonSuccess,
640
+ type JsonFormatOptions,
641
+ type JsonResponse,
642
+ } from "./formatters/json"
643
+
644
+ export { formatNdjson, printNdjson } from "./formatters/ndjson"
645
+
646
+ export {
647
+ formatAggregateTokenSummary,
648
+ formatChatSearchTable,
649
+ formatChatTable,
650
+ formatProjectsTable,
651
+ formatSessionsTable,
652
+ formatTokenSummary,
653
+ printAggregateTokenSummary,
654
+ printChatSearchTable,
655
+ printChatTable,
656
+ printProjectsTable,
657
+ printSessionsTable,
658
+ printTokenSummary,
659
+ type IndexedChatSearchResult,
660
+ type TableFormatOptions,
661
+ } from "./formatters/table"