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,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
+ }