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.
- package/PROJECT-SUMMARY.md +104 -24
- package/README.md +335 -7
- package/bun.lock +17 -1
- package/manage_opencode_projects.py +71 -66
- package/package.json +6 -3
- package/src/bin/opencode-manager.ts +133 -3
- package/src/cli/backup.ts +324 -0
- package/src/cli/commands/chat.ts +322 -0
- package/src/cli/commands/projects.ts +222 -0
- package/src/cli/commands/sessions.ts +495 -0
- package/src/cli/commands/tokens.ts +168 -0
- package/src/cli/commands/tui.ts +36 -0
- package/src/cli/errors.ts +259 -0
- package/src/cli/formatters/json.ts +184 -0
- package/src/cli/formatters/ndjson.ts +71 -0
- package/src/cli/formatters/table.ts +837 -0
- package/src/cli/index.ts +169 -0
- package/src/cli/output.ts +661 -0
- package/src/cli/resolvers.ts +249 -0
- package/src/lib/clipboard.ts +37 -0
- package/src/lib/opencode-data.ts +380 -1
- package/src/lib/search.ts +170 -0
- package/src/{opencode-tui.tsx → tui/app.tsx} +739 -105
- package/src/tui/args.ts +92 -0
- package/src/tui/index.tsx +46 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI error handling module.
|
|
3
|
+
*
|
|
4
|
+
* Provides standardized error classes and exit helpers for consistent
|
|
5
|
+
* error handling across all CLI commands.
|
|
6
|
+
*
|
|
7
|
+
* Exit codes:
|
|
8
|
+
* - 0: Success
|
|
9
|
+
* - 1: General error (unspecified)
|
|
10
|
+
* - 2: Usage error (e.g., missing --yes for destructive operations)
|
|
11
|
+
* - 3: Missing resource (e.g., invalid project/session ID)
|
|
12
|
+
* - 4: File operation failure (e.g., backup failed, delete failed)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { formatErrorOutput, type OutputFormat } from "./output"
|
|
16
|
+
|
|
17
|
+
// ========================
|
|
18
|
+
// Exit Codes
|
|
19
|
+
// ========================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Standardized CLI exit codes.
|
|
23
|
+
*/
|
|
24
|
+
export const ExitCode = {
|
|
25
|
+
/** Success */
|
|
26
|
+
SUCCESS: 0,
|
|
27
|
+
/** General error (unspecified) */
|
|
28
|
+
ERROR: 1,
|
|
29
|
+
/** Usage error (e.g., missing --yes for destructive operations) */
|
|
30
|
+
USAGE_ERROR: 2,
|
|
31
|
+
/** Missing resource (e.g., invalid project/session ID) */
|
|
32
|
+
NOT_FOUND: 3,
|
|
33
|
+
/** File operation failure (e.g., backup failed, delete failed) */
|
|
34
|
+
FILE_ERROR: 4,
|
|
35
|
+
} as const
|
|
36
|
+
|
|
37
|
+
export type ExitCodeValue = (typeof ExitCode)[keyof typeof ExitCode]
|
|
38
|
+
|
|
39
|
+
// ========================
|
|
40
|
+
// Error Classes
|
|
41
|
+
// ========================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Base class for CLI errors with exit codes.
|
|
45
|
+
*/
|
|
46
|
+
export class CLIError extends Error {
|
|
47
|
+
constructor(
|
|
48
|
+
message: string,
|
|
49
|
+
public readonly exitCode: ExitCodeValue = ExitCode.ERROR
|
|
50
|
+
) {
|
|
51
|
+
super(message)
|
|
52
|
+
this.name = "CLIError"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Usage error (exit code 2).
|
|
58
|
+
* Thrown when CLI usage is incorrect, such as missing required confirmation.
|
|
59
|
+
*/
|
|
60
|
+
export class UsageError extends CLIError {
|
|
61
|
+
constructor(message: string) {
|
|
62
|
+
super(message, ExitCode.USAGE_ERROR)
|
|
63
|
+
this.name = "UsageError"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Not found error (exit code 3).
|
|
69
|
+
* Thrown when a requested resource (project, session, message) doesn't exist.
|
|
70
|
+
*/
|
|
71
|
+
export class NotFoundError extends CLIError {
|
|
72
|
+
constructor(
|
|
73
|
+
message: string,
|
|
74
|
+
public readonly resourceType?: "project" | "session" | "message"
|
|
75
|
+
) {
|
|
76
|
+
super(message, ExitCode.NOT_FOUND)
|
|
77
|
+
this.name = "NotFoundError"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* File operation error (exit code 4).
|
|
83
|
+
* Thrown when a file system operation fails (backup, delete, copy, move).
|
|
84
|
+
*/
|
|
85
|
+
export class FileOperationError extends CLIError {
|
|
86
|
+
constructor(
|
|
87
|
+
message: string,
|
|
88
|
+
public readonly operation?: "backup" | "delete" | "copy" | "move" | "read" | "write"
|
|
89
|
+
) {
|
|
90
|
+
super(message, ExitCode.FILE_ERROR)
|
|
91
|
+
this.name = "FileOperationError"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ========================
|
|
96
|
+
// Exit Helpers
|
|
97
|
+
// ========================
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Exit the process with a formatted error message.
|
|
101
|
+
*
|
|
102
|
+
* @param error - Error message or Error object
|
|
103
|
+
* @param exitCode - Exit code (defaults to 1)
|
|
104
|
+
* @param format - Output format for error message
|
|
105
|
+
*/
|
|
106
|
+
export function exitWithError(
|
|
107
|
+
error: string | Error,
|
|
108
|
+
exitCode: ExitCodeValue = ExitCode.ERROR,
|
|
109
|
+
format: OutputFormat = "table"
|
|
110
|
+
): never {
|
|
111
|
+
console.error(formatErrorOutput(error, format))
|
|
112
|
+
process.exit(exitCode)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Exit the process with a CLIError, using its exit code.
|
|
117
|
+
*
|
|
118
|
+
* @param error - CLIError instance
|
|
119
|
+
* @param format - Output format for error message
|
|
120
|
+
*/
|
|
121
|
+
export function exitWithCLIError(
|
|
122
|
+
error: CLIError,
|
|
123
|
+
format: OutputFormat = "table"
|
|
124
|
+
): never {
|
|
125
|
+
console.error(formatErrorOutput(error, format))
|
|
126
|
+
process.exit(error.exitCode)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Exit with a usage error (exit code 2).
|
|
131
|
+
*
|
|
132
|
+
* @param message - Error message
|
|
133
|
+
* @param format - Output format
|
|
134
|
+
*/
|
|
135
|
+
export function exitUsageError(
|
|
136
|
+
message: string,
|
|
137
|
+
format: OutputFormat = "table"
|
|
138
|
+
): never {
|
|
139
|
+
exitWithError(message, ExitCode.USAGE_ERROR, format)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Exit with a not found error (exit code 3).
|
|
144
|
+
*
|
|
145
|
+
* @param message - Error message
|
|
146
|
+
* @param format - Output format
|
|
147
|
+
*/
|
|
148
|
+
export function exitNotFound(
|
|
149
|
+
message: string,
|
|
150
|
+
format: OutputFormat = "table"
|
|
151
|
+
): never {
|
|
152
|
+
exitWithError(message, ExitCode.NOT_FOUND, format)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Exit with a file operation error (exit code 4).
|
|
157
|
+
*
|
|
158
|
+
* @param message - Error message
|
|
159
|
+
* @param format - Output format
|
|
160
|
+
*/
|
|
161
|
+
export function exitFileError(
|
|
162
|
+
message: string,
|
|
163
|
+
format: OutputFormat = "table"
|
|
164
|
+
): never {
|
|
165
|
+
exitWithError(message, ExitCode.FILE_ERROR, format)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ========================
|
|
169
|
+
// Error Handling Utilities
|
|
170
|
+
// ========================
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Handle an error by exiting with the appropriate code.
|
|
174
|
+
* Recognizes CLIError subclasses and uses their exit codes.
|
|
175
|
+
*
|
|
176
|
+
* @param error - Error to handle
|
|
177
|
+
* @param format - Output format
|
|
178
|
+
*/
|
|
179
|
+
export function handleError(
|
|
180
|
+
error: unknown,
|
|
181
|
+
format: OutputFormat = "table"
|
|
182
|
+
): never {
|
|
183
|
+
if (error instanceof CLIError) {
|
|
184
|
+
exitWithCLIError(error, format)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (error instanceof Error) {
|
|
188
|
+
exitWithError(error.message, ExitCode.ERROR, format)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
exitWithError(String(error), ExitCode.ERROR, format)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Wrap an async function to catch errors and exit appropriately.
|
|
196
|
+
* Use this to wrap command handlers.
|
|
197
|
+
*
|
|
198
|
+
* @param fn - Async function to wrap
|
|
199
|
+
* @param format - Output format for errors
|
|
200
|
+
* @returns Wrapped function that handles errors
|
|
201
|
+
*/
|
|
202
|
+
export function withErrorHandling<T extends unknown[]>(
|
|
203
|
+
fn: (...args: T) => Promise<void>,
|
|
204
|
+
format: OutputFormat = "table"
|
|
205
|
+
): (...args: T) => Promise<void> {
|
|
206
|
+
return async (...args: T) => {
|
|
207
|
+
try {
|
|
208
|
+
await fn(...args)
|
|
209
|
+
} catch (error) {
|
|
210
|
+
handleError(error, format)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ========================
|
|
216
|
+
// Validation Helpers
|
|
217
|
+
// ========================
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Require confirmation for destructive operations.
|
|
221
|
+
* Throws UsageError if --yes flag is not provided.
|
|
222
|
+
*
|
|
223
|
+
* @param yes - Whether --yes flag was provided
|
|
224
|
+
* @param operation - Description of the operation for error message
|
|
225
|
+
*/
|
|
226
|
+
export function requireConfirmation(yes: boolean, operation: string): void {
|
|
227
|
+
if (!yes) {
|
|
228
|
+
throw new UsageError(
|
|
229
|
+
`${operation} requires --yes flag to confirm. Use --dry-run to preview changes.`
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Throw NotFoundError for a project.
|
|
236
|
+
*
|
|
237
|
+
* @param projectId - The project ID that wasn't found
|
|
238
|
+
*/
|
|
239
|
+
export function projectNotFound(projectId: string): never {
|
|
240
|
+
throw new NotFoundError(`Project not found: ${projectId}`, "project")
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Throw NotFoundError for a session.
|
|
245
|
+
*
|
|
246
|
+
* @param sessionId - The session ID that wasn't found
|
|
247
|
+
*/
|
|
248
|
+
export function sessionNotFound(sessionId: string): never {
|
|
249
|
+
throw new NotFoundError(`Session not found: ${sessionId}`, "session")
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Throw NotFoundError for a message.
|
|
254
|
+
*
|
|
255
|
+
* @param messageId - The message ID that wasn't found
|
|
256
|
+
*/
|
|
257
|
+
export function messageNotFound(messageId: string): never {
|
|
258
|
+
throw new NotFoundError(`Message not found: ${messageId}`, "message")
|
|
259
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON output formatter for CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Provides standard JSON output helpers for formatting single records,
|
|
5
|
+
* arrays, and structured responses with consistent serialization.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Options for JSON formatting.
|
|
10
|
+
*/
|
|
11
|
+
export interface JsonFormatOptions {
|
|
12
|
+
/** Pretty-print with indentation (default: true for TTY, false otherwise) */
|
|
13
|
+
pretty?: boolean
|
|
14
|
+
/** Indentation level for pretty printing (default: 2) */
|
|
15
|
+
indent?: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Standard JSON response envelope for CLI output.
|
|
20
|
+
*/
|
|
21
|
+
export interface JsonResponse<T> {
|
|
22
|
+
/** Whether the operation was successful */
|
|
23
|
+
ok: boolean
|
|
24
|
+
/** The data payload */
|
|
25
|
+
data?: T
|
|
26
|
+
/** Error message if operation failed */
|
|
27
|
+
error?: string
|
|
28
|
+
/** Optional metadata about the response */
|
|
29
|
+
meta?: {
|
|
30
|
+
/** Total count of items (for paginated results) */
|
|
31
|
+
count?: number
|
|
32
|
+
/** Limit applied to results */
|
|
33
|
+
limit?: number
|
|
34
|
+
/** Whether results were truncated due to limit */
|
|
35
|
+
truncated?: boolean
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Determine if output should be pretty-printed.
|
|
41
|
+
* Defaults to pretty for TTY, compact for piped output.
|
|
42
|
+
*/
|
|
43
|
+
function shouldPrettyPrint(options?: JsonFormatOptions): boolean {
|
|
44
|
+
if (options?.pretty !== undefined) {
|
|
45
|
+
return options.pretty
|
|
46
|
+
}
|
|
47
|
+
// Default: pretty for TTY, compact for pipes
|
|
48
|
+
return process.stdout.isTTY ?? false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the indentation string for pretty printing.
|
|
53
|
+
*/
|
|
54
|
+
function getIndent(options?: JsonFormatOptions): number | undefined {
|
|
55
|
+
if (!shouldPrettyPrint(options)) {
|
|
56
|
+
return undefined
|
|
57
|
+
}
|
|
58
|
+
return options?.indent ?? 2
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Custom replacer function to handle special types during JSON serialization.
|
|
63
|
+
* - Converts Date objects to ISO strings
|
|
64
|
+
* - Handles undefined values
|
|
65
|
+
*/
|
|
66
|
+
function jsonReplacer(_key: string, value: unknown): unknown {
|
|
67
|
+
if (value instanceof Date) {
|
|
68
|
+
return value.toISOString()
|
|
69
|
+
}
|
|
70
|
+
return value
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Format a single value as JSON.
|
|
75
|
+
*/
|
|
76
|
+
export function formatJson<T>(data: T, options?: JsonFormatOptions): string {
|
|
77
|
+
const indent = getIndent(options)
|
|
78
|
+
return JSON.stringify(data, jsonReplacer, indent)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Format an array of values as JSON.
|
|
83
|
+
*/
|
|
84
|
+
export function formatJsonArray<T>(data: T[], options?: JsonFormatOptions): string {
|
|
85
|
+
const indent = getIndent(options)
|
|
86
|
+
return JSON.stringify(data, jsonReplacer, indent)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Format a successful response with data.
|
|
91
|
+
*/
|
|
92
|
+
export function formatJsonSuccess<T>(
|
|
93
|
+
data: T,
|
|
94
|
+
meta?: JsonResponse<T>["meta"],
|
|
95
|
+
options?: JsonFormatOptions
|
|
96
|
+
): string {
|
|
97
|
+
const response: JsonResponse<T> = {
|
|
98
|
+
ok: true,
|
|
99
|
+
data,
|
|
100
|
+
...(meta && { meta }),
|
|
101
|
+
}
|
|
102
|
+
return formatJson(response, options)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Format a successful response with an array of data.
|
|
107
|
+
* Automatically populates count metadata.
|
|
108
|
+
*/
|
|
109
|
+
export function formatJsonArraySuccess<T>(
|
|
110
|
+
data: T[],
|
|
111
|
+
meta?: Omit<NonNullable<JsonResponse<T[]>["meta"]>, "count">,
|
|
112
|
+
options?: JsonFormatOptions
|
|
113
|
+
): string {
|
|
114
|
+
const response: JsonResponse<T[]> = {
|
|
115
|
+
ok: true,
|
|
116
|
+
data,
|
|
117
|
+
meta: {
|
|
118
|
+
count: data.length,
|
|
119
|
+
...meta,
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
return formatJson(response, options)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Format an error response.
|
|
127
|
+
*/
|
|
128
|
+
export function formatJsonError(
|
|
129
|
+
error: string | Error,
|
|
130
|
+
options?: JsonFormatOptions
|
|
131
|
+
): string {
|
|
132
|
+
const message = error instanceof Error ? error.message : error
|
|
133
|
+
const response: JsonResponse<never> = {
|
|
134
|
+
ok: false,
|
|
135
|
+
error: message,
|
|
136
|
+
}
|
|
137
|
+
return formatJson(response, options)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Print JSON to stdout.
|
|
142
|
+
*/
|
|
143
|
+
export function printJson<T>(data: T, options?: JsonFormatOptions): void {
|
|
144
|
+
console.log(formatJson(data, options))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Print a JSON array to stdout.
|
|
149
|
+
*/
|
|
150
|
+
export function printJsonArray<T>(data: T[], options?: JsonFormatOptions): void {
|
|
151
|
+
console.log(formatJsonArray(data, options))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Print a success response to stdout.
|
|
156
|
+
*/
|
|
157
|
+
export function printJsonSuccess<T>(
|
|
158
|
+
data: T,
|
|
159
|
+
meta?: JsonResponse<T>["meta"],
|
|
160
|
+
options?: JsonFormatOptions
|
|
161
|
+
): void {
|
|
162
|
+
console.log(formatJsonSuccess(data, meta, options))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Print a success response with array data to stdout.
|
|
167
|
+
*/
|
|
168
|
+
export function printJsonArraySuccess<T>(
|
|
169
|
+
data: T[],
|
|
170
|
+
meta?: Omit<NonNullable<JsonResponse<T[]>["meta"]>, "count">,
|
|
171
|
+
options?: JsonFormatOptions
|
|
172
|
+
): void {
|
|
173
|
+
console.log(formatJsonArraySuccess(data, meta, options))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Print an error response to stdout.
|
|
178
|
+
*/
|
|
179
|
+
export function printJsonError(
|
|
180
|
+
error: string | Error,
|
|
181
|
+
options?: JsonFormatOptions
|
|
182
|
+
): void {
|
|
183
|
+
console.log(formatJsonError(error, options))
|
|
184
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NDJSON (Newline Delimited JSON) output formatter for CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Provides streaming-friendly output with one JSON record per line.
|
|
5
|
+
* Ideal for piping to tools like `jq` or processing large datasets.
|
|
6
|
+
*
|
|
7
|
+
* @see https://github.com/ndjson/ndjson-spec
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Custom replacer function to handle special types during JSON serialization.
|
|
12
|
+
* - Converts Date objects to ISO strings
|
|
13
|
+
*/
|
|
14
|
+
function jsonReplacer(_key: string, value: unknown): unknown {
|
|
15
|
+
if (value instanceof Date) {
|
|
16
|
+
return value.toISOString()
|
|
17
|
+
}
|
|
18
|
+
return value
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Format a single record as an NDJSON line (compact JSON with no trailing newline).
|
|
23
|
+
*/
|
|
24
|
+
export function formatNdjsonLine<T>(record: T): string {
|
|
25
|
+
return JSON.stringify(record, jsonReplacer)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Format an array of records as NDJSON (one JSON object per line).
|
|
30
|
+
* Each line is a complete, self-contained JSON object.
|
|
31
|
+
*/
|
|
32
|
+
export function formatNdjson<T>(records: T[]): string {
|
|
33
|
+
return records.map((record) => formatNdjsonLine(record)).join("\n")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generator function to yield NDJSON lines one at a time.
|
|
38
|
+
* Useful for streaming large datasets without buffering the entire output.
|
|
39
|
+
*/
|
|
40
|
+
export function* streamNdjson<T>(records: Iterable<T>): Generator<string, void, unknown> {
|
|
41
|
+
for (const record of records) {
|
|
42
|
+
yield formatNdjsonLine(record)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Print a single record as an NDJSON line to stdout.
|
|
48
|
+
*/
|
|
49
|
+
export function printNdjsonLine<T>(record: T): void {
|
|
50
|
+
console.log(formatNdjsonLine(record))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Print an array of records as NDJSON to stdout.
|
|
55
|
+
* Each record is printed on its own line.
|
|
56
|
+
*/
|
|
57
|
+
export function printNdjson<T>(records: T[]): void {
|
|
58
|
+
for (const record of records) {
|
|
59
|
+
console.log(formatNdjsonLine(record))
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Stream records to stdout as NDJSON.
|
|
65
|
+
* Flushes each line immediately, making it suitable for real-time output.
|
|
66
|
+
*/
|
|
67
|
+
export function streamPrintNdjson<T>(records: Iterable<T>): void {
|
|
68
|
+
for (const record of records) {
|
|
69
|
+
console.log(formatNdjsonLine(record))
|
|
70
|
+
}
|
|
71
|
+
}
|