@sylphx/flow 1.0.1 → 1.0.3

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.
Files changed (229) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +10 -9
  3. package/src/commands/codebase-command.ts +168 -0
  4. package/src/commands/flow-command.ts +1137 -0
  5. package/src/commands/flow-orchestrator.ts +296 -0
  6. package/src/commands/hook-command.ts +444 -0
  7. package/src/commands/init-command.ts +92 -0
  8. package/src/commands/init-core.ts +322 -0
  9. package/src/commands/knowledge-command.ts +161 -0
  10. package/src/commands/run-command.ts +120 -0
  11. package/src/components/benchmark-monitor.tsx +331 -0
  12. package/src/components/reindex-progress.tsx +261 -0
  13. package/src/composables/functional/index.ts +14 -0
  14. package/src/composables/functional/useEnvironment.ts +171 -0
  15. package/src/composables/functional/useFileSystem.ts +139 -0
  16. package/src/composables/index.ts +5 -0
  17. package/src/composables/useEnv.ts +13 -0
  18. package/src/composables/useRuntimeConfig.ts +27 -0
  19. package/src/composables/useTargetConfig.ts +45 -0
  20. package/src/config/ai-config.ts +376 -0
  21. package/src/config/constants.ts +35 -0
  22. package/src/config/index.ts +27 -0
  23. package/src/config/rules.ts +43 -0
  24. package/src/config/servers.ts +371 -0
  25. package/src/config/targets.ts +126 -0
  26. package/src/core/agent-loader.ts +141 -0
  27. package/src/core/agent-manager.ts +174 -0
  28. package/src/core/ai-sdk.ts +603 -0
  29. package/src/core/app-factory.ts +381 -0
  30. package/src/core/builtin-agents.ts +9 -0
  31. package/src/core/command-system.ts +550 -0
  32. package/src/core/config-system.ts +550 -0
  33. package/src/core/connection-pool.ts +390 -0
  34. package/src/core/di-container.ts +155 -0
  35. package/src/core/error-handling.ts +519 -0
  36. package/src/core/formatting/bytes.test.ts +115 -0
  37. package/src/core/formatting/bytes.ts +64 -0
  38. package/src/core/functional/async.ts +313 -0
  39. package/src/core/functional/either.ts +109 -0
  40. package/src/core/functional/error-handler.ts +135 -0
  41. package/src/core/functional/error-types.ts +311 -0
  42. package/src/core/functional/index.ts +19 -0
  43. package/src/core/functional/option.ts +142 -0
  44. package/src/core/functional/pipe.ts +189 -0
  45. package/src/core/functional/result.ts +204 -0
  46. package/src/core/functional/validation.ts +138 -0
  47. package/src/core/headless-display.ts +96 -0
  48. package/src/core/index.ts +6 -0
  49. package/src/core/installers/file-installer.ts +303 -0
  50. package/src/core/installers/mcp-installer.ts +213 -0
  51. package/src/core/interfaces/index.ts +22 -0
  52. package/src/core/interfaces/repository.interface.ts +91 -0
  53. package/src/core/interfaces/service.interface.ts +133 -0
  54. package/src/core/interfaces.ts +129 -0
  55. package/src/core/loop-controller.ts +200 -0
  56. package/src/core/result.ts +351 -0
  57. package/src/core/rule-loader.ts +147 -0
  58. package/src/core/rule-manager.ts +240 -0
  59. package/src/core/service-config.ts +252 -0
  60. package/src/core/session-service.ts +121 -0
  61. package/src/core/state-detector.ts +389 -0
  62. package/src/core/storage-factory.ts +115 -0
  63. package/src/core/stream-handler.ts +288 -0
  64. package/src/core/target-manager.ts +161 -0
  65. package/src/core/type-utils.ts +427 -0
  66. package/src/core/unified-storage.ts +456 -0
  67. package/src/core/upgrade-manager.ts +300 -0
  68. package/src/core/validation/limit.test.ts +155 -0
  69. package/src/core/validation/limit.ts +46 -0
  70. package/src/core/validation/query.test.ts +44 -0
  71. package/src/core/validation/query.ts +20 -0
  72. package/src/db/auto-migrate.ts +322 -0
  73. package/src/db/base-database-client.ts +144 -0
  74. package/src/db/cache-db.ts +218 -0
  75. package/src/db/cache-schema.ts +75 -0
  76. package/src/db/database.ts +70 -0
  77. package/src/db/index.ts +252 -0
  78. package/src/db/memory-db.ts +153 -0
  79. package/src/db/memory-schema.ts +29 -0
  80. package/src/db/schema.ts +289 -0
  81. package/src/db/session-repository.ts +733 -0
  82. package/src/domains/codebase/index.ts +5 -0
  83. package/src/domains/codebase/tools.ts +139 -0
  84. package/src/domains/index.ts +8 -0
  85. package/src/domains/knowledge/index.ts +10 -0
  86. package/src/domains/knowledge/resources.ts +537 -0
  87. package/src/domains/knowledge/tools.ts +174 -0
  88. package/src/domains/utilities/index.ts +6 -0
  89. package/src/domains/utilities/time/index.ts +5 -0
  90. package/src/domains/utilities/time/tools.ts +291 -0
  91. package/src/index.ts +211 -0
  92. package/src/services/agent-service.ts +273 -0
  93. package/src/services/claude-config-service.ts +252 -0
  94. package/src/services/config-service.ts +258 -0
  95. package/src/services/evaluation-service.ts +271 -0
  96. package/src/services/functional/evaluation-logic.ts +296 -0
  97. package/src/services/functional/file-processor.ts +273 -0
  98. package/src/services/functional/index.ts +12 -0
  99. package/src/services/index.ts +13 -0
  100. package/src/services/mcp-service.ts +432 -0
  101. package/src/services/memory.service.ts +476 -0
  102. package/src/services/search/base-indexer.ts +156 -0
  103. package/src/services/search/codebase-indexer-types.ts +38 -0
  104. package/src/services/search/codebase-indexer.ts +647 -0
  105. package/src/services/search/embeddings-provider.ts +455 -0
  106. package/src/services/search/embeddings.ts +316 -0
  107. package/src/services/search/functional-indexer.ts +323 -0
  108. package/src/services/search/index.ts +27 -0
  109. package/src/services/search/indexer.ts +380 -0
  110. package/src/services/search/knowledge-indexer.ts +422 -0
  111. package/src/services/search/semantic-search.ts +244 -0
  112. package/src/services/search/tfidf.ts +559 -0
  113. package/src/services/search/unified-search-service.ts +888 -0
  114. package/src/services/smart-config-service.ts +385 -0
  115. package/src/services/storage/cache-storage.ts +487 -0
  116. package/src/services/storage/drizzle-storage.ts +581 -0
  117. package/src/services/storage/index.ts +15 -0
  118. package/src/services/storage/lancedb-vector-storage.ts +494 -0
  119. package/src/services/storage/memory-storage.ts +268 -0
  120. package/src/services/storage/separated-storage.ts +467 -0
  121. package/src/services/storage/vector-storage.ts +13 -0
  122. package/src/shared/agents/index.ts +63 -0
  123. package/src/shared/files/index.ts +99 -0
  124. package/src/shared/index.ts +32 -0
  125. package/src/shared/logging/index.ts +24 -0
  126. package/src/shared/processing/index.ts +153 -0
  127. package/src/shared/types/index.ts +25 -0
  128. package/src/targets/claude-code.ts +574 -0
  129. package/src/targets/functional/claude-code-logic.ts +185 -0
  130. package/src/targets/functional/index.ts +6 -0
  131. package/src/targets/opencode.ts +529 -0
  132. package/src/types/agent.types.ts +32 -0
  133. package/src/types/api/batch.ts +108 -0
  134. package/src/types/api/errors.ts +118 -0
  135. package/src/types/api/index.ts +55 -0
  136. package/src/types/api/requests.ts +76 -0
  137. package/src/types/api/responses.ts +180 -0
  138. package/src/types/api/websockets.ts +85 -0
  139. package/src/types/api.types.ts +9 -0
  140. package/src/types/benchmark.ts +49 -0
  141. package/src/types/cli.types.ts +87 -0
  142. package/src/types/common.types.ts +35 -0
  143. package/src/types/database.types.ts +510 -0
  144. package/src/types/mcp-config.types.ts +448 -0
  145. package/src/types/mcp.types.ts +69 -0
  146. package/src/types/memory-types.ts +63 -0
  147. package/src/types/provider.types.ts +28 -0
  148. package/src/types/rule.types.ts +24 -0
  149. package/src/types/session.types.ts +214 -0
  150. package/src/types/target-config.types.ts +295 -0
  151. package/src/types/target.types.ts +140 -0
  152. package/src/types/todo.types.ts +25 -0
  153. package/src/types.ts +40 -0
  154. package/src/utils/advanced-tokenizer.ts +191 -0
  155. package/src/utils/agent-enhancer.ts +114 -0
  156. package/src/utils/ai-model-fetcher.ts +19 -0
  157. package/src/utils/async-file-operations.ts +516 -0
  158. package/src/utils/audio-player.ts +345 -0
  159. package/src/utils/cli-output.ts +266 -0
  160. package/src/utils/codebase-helpers.ts +211 -0
  161. package/src/utils/console-ui.ts +79 -0
  162. package/src/utils/database-errors.ts +140 -0
  163. package/src/utils/debug-logger.ts +49 -0
  164. package/src/utils/error-handler.ts +53 -0
  165. package/src/utils/file-operations.ts +310 -0
  166. package/src/utils/file-scanner.ts +259 -0
  167. package/src/utils/functional/array.ts +355 -0
  168. package/src/utils/functional/index.ts +15 -0
  169. package/src/utils/functional/object.ts +279 -0
  170. package/src/utils/functional/string.ts +281 -0
  171. package/src/utils/functional.ts +543 -0
  172. package/src/utils/help.ts +20 -0
  173. package/src/utils/immutable-cache.ts +106 -0
  174. package/src/utils/index.ts +78 -0
  175. package/src/utils/jsonc.ts +158 -0
  176. package/src/utils/logger.ts +396 -0
  177. package/src/utils/mcp-config.ts +249 -0
  178. package/src/utils/memory-tui.ts +414 -0
  179. package/src/utils/models-dev.ts +91 -0
  180. package/src/utils/notifications.ts +169 -0
  181. package/src/utils/object-utils.ts +51 -0
  182. package/src/utils/parallel-operations.ts +487 -0
  183. package/src/utils/paths.ts +143 -0
  184. package/src/utils/process-manager.ts +155 -0
  185. package/src/utils/prompts.ts +120 -0
  186. package/src/utils/search-tool-builder.ts +214 -0
  187. package/src/utils/secret-utils.ts +179 -0
  188. package/src/utils/security.ts +537 -0
  189. package/src/utils/session-manager.ts +168 -0
  190. package/src/utils/session-title.ts +87 -0
  191. package/src/utils/settings.ts +182 -0
  192. package/src/utils/simplified-errors.ts +410 -0
  193. package/src/utils/sync-utils.ts +159 -0
  194. package/src/utils/target-config.ts +570 -0
  195. package/src/utils/target-utils.ts +394 -0
  196. package/src/utils/template-engine.ts +94 -0
  197. package/src/utils/test-audio.ts +71 -0
  198. package/src/utils/todo-context.ts +46 -0
  199. package/src/utils/token-counter.ts +288 -0
  200. package/dist/index.d.ts +0 -10
  201. package/dist/index.js +0 -59554
  202. package/dist/lancedb.linux-x64-gnu-b7f0jgsz.node +0 -0
  203. package/dist/lancedb.linux-x64-musl-tgcv22rx.node +0 -0
  204. package/dist/shared/chunk-25dwp0dp.js +0 -89
  205. package/dist/shared/chunk-3pjb6063.js +0 -208
  206. package/dist/shared/chunk-4d6ydpw7.js +0 -2854
  207. package/dist/shared/chunk-4wjcadjk.js +0 -225
  208. package/dist/shared/chunk-5j4w74t6.js +0 -30
  209. package/dist/shared/chunk-5j8m3dh3.js +0 -58
  210. package/dist/shared/chunk-5thh3qem.js +0 -91
  211. package/dist/shared/chunk-6g9xy73m.js +0 -252
  212. package/dist/shared/chunk-7eq34c42.js +0 -23
  213. package/dist/shared/chunk-c2gwgx3r.js +0 -115
  214. package/dist/shared/chunk-cjd3mk4c.js +0 -1320
  215. package/dist/shared/chunk-g5cv6703.js +0 -368
  216. package/dist/shared/chunk-hpkhykhq.js +0 -574
  217. package/dist/shared/chunk-m2322pdk.js +0 -122
  218. package/dist/shared/chunk-nd5fdvaq.js +0 -26
  219. package/dist/shared/chunk-pgd3m6zf.js +0 -108
  220. package/dist/shared/chunk-qk8n91hw.js +0 -494
  221. package/dist/shared/chunk-rkkn8szp.js +0 -16855
  222. package/dist/shared/chunk-t16rfxh0.js +0 -61
  223. package/dist/shared/chunk-t4fbfa5v.js +0 -19
  224. package/dist/shared/chunk-t77h86w6.js +0 -276
  225. package/dist/shared/chunk-v0ez4aef.js +0 -71
  226. package/dist/shared/chunk-v29j2r3s.js +0 -32051
  227. package/dist/shared/chunk-vfbc6ew5.js +0 -765
  228. package/dist/shared/chunk-vmeqwm1c.js +0 -204
  229. package/dist/shared/chunk-x66eh37x.js +0 -137
@@ -0,0 +1,519 @@
1
+ /**
2
+ * Error Handling - 統一錯誤處理
3
+ * Functional, composable error handling system
4
+ */
5
+
6
+ import { logger } from '../utils/logger.js';
7
+ import type { Result } from './result.js';
8
+
9
+ /**
10
+ * Base error class
11
+ */
12
+ export class BaseError extends Error {
13
+ public readonly code: string;
14
+ public readonly statusCode: number;
15
+ public readonly details?: Record<string, unknown>;
16
+
17
+ constructor(
18
+ message: string,
19
+ code: string,
20
+ statusCode = 500,
21
+ details?: Record<string, unknown>
22
+ ) {
23
+ super(message);
24
+ this.name = this.constructor.name;
25
+ this.code = code;
26
+ this.statusCode = statusCode;
27
+ this.details = details;
28
+
29
+ // Maintains proper stack trace for where our error was thrown
30
+ Error.captureStackTrace(this, this.constructor);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Validation error
36
+ */
37
+ export class ValidationError extends BaseError {
38
+ constructor(message: string, details?: Record<string, unknown>) {
39
+ super(message, 'VALIDATION_ERROR', 400, details);
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Configuration error
45
+ */
46
+ export class ConfigurationError extends BaseError {
47
+ constructor(message: string, details?: Record<string, unknown>) {
48
+ super(message, 'CONFIGURATION_ERROR', 500, details);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Storage error
54
+ */
55
+ export class StorageError extends BaseError {
56
+ constructor(message: string, operation?: string, details?: Record<string, unknown>) {
57
+ super(message, 'STORAGE_ERROR', 500, { operation, ...details });
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Network error
63
+ */
64
+ export class NetworkError extends BaseError {
65
+ constructor(message: string, details?: Record<string, unknown>) {
66
+ super(message, 'NETWORK_ERROR', 503, details);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Authentication error
72
+ */
73
+ export class AuthenticationError extends BaseError {
74
+ constructor(message: string, details?: Record<string, unknown>) {
75
+ super(message, 'AUTHENTICATION_ERROR', 401, details);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Authorization error
81
+ */
82
+ export class AuthorizationError extends BaseError {
83
+ constructor(message: string, details?: Record<string, unknown>) {
84
+ super(message, 'AUTHORIZATION_ERROR', 403, details);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Not found error
90
+ */
91
+ export class NotFoundError extends BaseError {
92
+ constructor(message: string, resource?: string, details?: Record<string, unknown>) {
93
+ super(message, 'NOT_FOUND', 404, { resource, ...details });
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Timeout error
99
+ */
100
+ export class TimeoutError extends BaseError {
101
+ constructor(message: string, timeout?: number, details?: Record<string, unknown>) {
102
+ super(message, 'TIMEOUT_ERROR', 408, { timeout, ...details });
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Error types
108
+ */
109
+ export const ErrorTypes = {
110
+ ValidationError,
111
+ ConfigurationError,
112
+ StorageError,
113
+ NetworkError,
114
+ AuthenticationError,
115
+ AuthorizationError,
116
+ NotFoundError,
117
+ TimeoutError,
118
+ } as const;
119
+
120
+ /**
121
+ * Error codes
122
+ */
123
+ export const ErrorCodes = {
124
+ // Validation errors
125
+ INVALID_INPUT: 'INVALID_INPUT',
126
+ MISSING_REQUIRED_FIELD: 'MISSING_REQUIRED_FIELD',
127
+ INVALID_FORMAT: 'INVALID_FORMAT',
128
+
129
+ // Configuration errors
130
+ MISSING_CONFIG: 'MISSING_CONFIG',
131
+ INVALID_CONFIG: 'INVALID_CONFIG',
132
+ CONFIG_PARSE_ERROR: 'CONFIG_PARSE_ERROR',
133
+
134
+ // Storage errors
135
+ STORAGE_CONNECTION_FAILED: 'STORAGE_CONNECTION_FAILED',
136
+ STORAGE_OPERATION_FAILED: 'STORAGE_OPERATION_FAILED',
137
+ STORAGE_TIMEOUT: 'STORAGE_TIMEOUT',
138
+ STORAGE_FULL: 'STORAGE_FULL',
139
+
140
+ // Network errors
141
+ CONNECTION_FAILED: 'CONNECTION_FAILED',
142
+ REQUEST_FAILED: 'REQUEST_FAILED',
143
+ REQUEST_TIMEOUT: 'REQUEST_TIMEOUT',
144
+ RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',
145
+
146
+ // Authentication errors
147
+ UNAUTHORIZED: 'UNAUTHORIZED',
148
+ INVALID_CREDENTIALS: 'INVALID_CREDENTIALS',
149
+ TOKEN_EXPIRED: 'TOKEN_EXPIRED',
150
+
151
+ // Authorization errors
152
+ FORBIDDEN: 'FORBIDDEN',
153
+ INSUFFICIENT_PERMISSIONS: 'INSUFFICIENT_PERMISSIONS',
154
+
155
+ // Not found errors
156
+ RESOURCE_NOT_FOUND: 'RESOURCE_NOT_FOUND',
157
+ ENDPOINT_NOT_FOUND: 'ENDPOINT_NOT_FOUND',
158
+
159
+ // System errors
160
+ INTERNAL_ERROR: 'INTERNAL_ERROR',
161
+ SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
162
+ TIMEOUT: 'TIMEOUT',
163
+ } as const;
164
+
165
+ /**
166
+ * Error handler interface
167
+ */
168
+ export interface ErrorHandler {
169
+ canHandle(error: Error): boolean;
170
+ handle(error: Error): void | Promise<void>;
171
+ }
172
+
173
+ /**
174
+ * Logger error handler
175
+ */
176
+ export class LoggerErrorHandler implements ErrorHandler {
177
+ constructor(private level: 'error' | 'warn' | 'info' | 'debug' = 'error') {}
178
+
179
+ canHandle(error: Error): boolean {
180
+ return true; // Logger can handle all errors
181
+ }
182
+
183
+ handle(error: Error): void {
184
+ const errorData = {
185
+ name: error.name,
186
+ message: error.message,
187
+ code: (error as BaseError).code,
188
+ statusCode: (error as BaseError).statusCode,
189
+ stack: error.stack,
190
+ details: (error as BaseError).details,
191
+ };
192
+
193
+ switch (this.level) {
194
+ case 'error':
195
+ logger.error(error.message, errorData);
196
+ break;
197
+ case 'warn':
198
+ logger.warn(error.message, errorData);
199
+ break;
200
+ case 'info':
201
+ logger.info(error.message, errorData);
202
+ break;
203
+ case 'debug':
204
+ logger.debug(error.message, errorData);
205
+ break;
206
+ }
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Console error handler
212
+ */
213
+ export class ConsoleErrorHandler implements ErrorHandler {
214
+ canHandle(error: Error): boolean {
215
+ return true; // Console can handle all errors
216
+ }
217
+
218
+ handle(error: Error): void {
219
+ if (error instanceof BaseError) {
220
+ console.error(`[${error.code}] ${error.message}`);
221
+ if (error.details) {
222
+ console.error('Details:', error.details);
223
+ }
224
+ } else {
225
+ console.error(error.message);
226
+ }
227
+
228
+ if (process.env.NODE_ENV === 'development' && error.stack) {
229
+ console.error('\nStack trace:');
230
+ console.error(error.stack);
231
+ }
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Error handler chain
237
+ */
238
+ export class ErrorHandlerChain {
239
+ private handlers: ErrorHandler[] = [];
240
+
241
+ constructor(handlers: ErrorHandler[] = []) {
242
+ this.handlers = handlers;
243
+ }
244
+
245
+ addHandler(handler: ErrorHandler): void {
246
+ this.handlers.push(handler);
247
+ }
248
+
249
+ removeHandler(handler: ErrorHandler): void {
250
+ const index = this.handlers.indexOf(handler);
251
+ if (index > -1) {
252
+ this.handlers.splice(index, 1);
253
+ }
254
+ }
255
+
256
+ async handle(error: Error): Promise<void> {
257
+ for (const handler of this.handlers) {
258
+ if (handler.canHandle(error)) {
259
+ await handler.handle(error);
260
+ return;
261
+ }
262
+ }
263
+
264
+ // If no handler can handle the error, use default console handler
265
+ new ConsoleErrorHandler().handle(error);
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Global error handler
271
+ */
272
+ export const globalErrorHandler = new ErrorHandlerChain([
273
+ new LoggerErrorHandler('error'),
274
+ ]);
275
+
276
+ /**
277
+ * Set up global error handlers
278
+ */
279
+ export function setupGlobalErrorHandlers(): void {
280
+ // Handle uncaught exceptions
281
+ process.on('uncaughtException', (error: Error) => {
282
+ logger.error('Uncaught Exception:', { error: error.message, stack: error.stack });
283
+ globalErrorHandler.handle(error);
284
+ process.exit(1);
285
+ });
286
+
287
+ // Handle unhandled promise rejections
288
+ process.on('unhandledRejection', (reason: unknown, promise: Promise<unknown>) => {
289
+ const error = reason instanceof Error ? reason : new Error(String(reason));
290
+ logger.error('Unhandled Rejection:', { error: error.message, reason, promise });
291
+ globalErrorHandler.handle(error);
292
+ });
293
+
294
+ // Handle process termination
295
+ process.on('SIGINT', () => {
296
+ logger.info('Process terminated by user');
297
+ process.exit(0);
298
+ });
299
+
300
+ process.on('SIGTERM', () => {
301
+ logger.info('Process terminated');
302
+ process.exit(0);
303
+ });
304
+ }
305
+
306
+ /**
307
+ * Safe function wrapper with error handling
308
+ */
309
+ export async function withErrorHandling<T>(
310
+ fn: () => Promise<T>,
311
+ errorHandler?: (error: Error) => void | Promise<void>
312
+ ): Promise<Result<T>> {
313
+ try {
314
+ const data = await fn();
315
+ return { success: true, data };
316
+ } catch (error) {
317
+ const errorObj = error instanceof Error ? error : new Error(String(error));
318
+
319
+ if (errorHandler) {
320
+ await errorHandler(errorObj);
321
+ } else {
322
+ await globalErrorHandler.handle(errorObj);
323
+ }
324
+
325
+ return { success: false, error: errorObj };
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Safe sync function wrapper with error handling
331
+ */
332
+ export function withSyncErrorHandling<T>(
333
+ fn: () => T,
334
+ errorHandler?: (error: Error) => void | Promise<void>
335
+ ): Result<T> {
336
+ try {
337
+ const data = fn();
338
+ return { success: true, data };
339
+ } catch (error) {
340
+ const errorObj = error instanceof Error ? error : new Error(String(error));
341
+
342
+ if (errorHandler) {
343
+ // For sync error handlers, we need to handle them asynchronously
344
+ void errorHandler(errorObj);
345
+ } else {
346
+ void globalErrorHandler.handle(errorObj);
347
+ }
348
+
349
+ return { success: false, error: errorObj };
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Retry function with error handling
355
+ */
356
+ export async function withRetry<T>(
357
+ fn: () => Promise<T>,
358
+ options: {
359
+ maxAttempts?: number;
360
+ delay?: number;
361
+ backoff?: 'linear' | 'exponential';
362
+ retryableErrors?: string[];
363
+ onRetry?: (attempt: number, error: Error) => void;
364
+ } = {}
365
+ ): Promise<Result<T>> {
366
+ const {
367
+ maxAttempts = 3,
368
+ delay = 1000,
369
+ backoff = 'exponential',
370
+ retryableErrors = [],
371
+ onRetry,
372
+ } = options;
373
+
374
+ let lastError: Error;
375
+
376
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
377
+ try {
378
+ const data = await fn();
379
+ return { success: true, data };
380
+ } catch (error) {
381
+ lastError = error instanceof Error ? error : new Error(String(error));
382
+
383
+ // Check if error is retryable
384
+ const isRetryable = retryableErrors.length === 0 ||
385
+ retryableErrors.includes((lastError as BaseError).code) ||
386
+ retryableErrors.includes(lastError.constructor.name);
387
+
388
+ if (!isRetryable || attempt === maxAttempts) {
389
+ await globalErrorHandler.handle(lastError);
390
+ return { success: false, error: lastError };
391
+ }
392
+
393
+ // Call retry callback
394
+ if (onRetry) {
395
+ onRetry(attempt, lastError);
396
+ }
397
+
398
+ // Calculate delay
399
+ const retryDelay = backoff === 'exponential'
400
+ ? delay * Math.pow(2, attempt - 1)
401
+ : delay * attempt;
402
+
403
+ // Wait before retry
404
+ await new Promise(resolve => setTimeout(resolve, retryDelay));
405
+ }
406
+ }
407
+
408
+ await globalErrorHandler.handle(lastError!);
409
+ return { success: false, error: lastError! };
410
+ }
411
+
412
+ /**
413
+ * Timeout wrapper
414
+ */
415
+ export async function withTimeout<T>(
416
+ fn: () => Promise<T>,
417
+ timeoutMs: number,
418
+ timeoutMessage = 'Operation timed out'
419
+ ): Promise<Result<T>> {
420
+ return new Promise((resolve) => {
421
+ const timeoutId = setTimeout(() => {
422
+ resolve({
423
+ success: false,
424
+ error: new TimeoutError(timeoutMessage, timeoutMs),
425
+ });
426
+ }, timeoutMs);
427
+
428
+ fn()
429
+ .then((data) => {
430
+ clearTimeout(timeoutId);
431
+ resolve({ success: true, data });
432
+ })
433
+ .catch((error) => {
434
+ clearTimeout(timeoutId);
435
+ resolve({
436
+ success: false,
437
+ error: error instanceof Error ? error : new Error(String(error)),
438
+ });
439
+ });
440
+ });
441
+ }
442
+
443
+ /**
444
+ * Circuit breaker pattern
445
+ */
446
+ export class CircuitBreaker {
447
+ private failures = 0;
448
+ private lastFailureTime = 0;
449
+ private state: 'closed' | 'open' | 'half-open' = 'closed';
450
+
451
+ constructor(
452
+ private options: {
453
+ failureThreshold?: number;
454
+ recoveryTimeMs?: number;
455
+ monitoringPeriodMs?: number;
456
+ } = {}
457
+ ) {
458
+ const {
459
+ failureThreshold = 5,
460
+ recoveryTimeMs = 60000,
461
+ monitoringPeriodMs = 10000,
462
+ } = options;
463
+
464
+ this.options = { failureThreshold, recoveryTimeMs, monitoringPeriodMs };
465
+ }
466
+
467
+ async execute<T>(fn: () => Promise<T>): Promise<Result<T>> {
468
+ if (this.state === 'open') {
469
+ if (Date.now() - this.lastFailureTime > this.options.recoveryTimeMs!) {
470
+ this.state = 'half-open';
471
+ } else {
472
+ return {
473
+ success: false,
474
+ error: new Error('Circuit breaker is open'),
475
+ };
476
+ }
477
+ }
478
+
479
+ try {
480
+ const result = await fn();
481
+ this.onSuccess();
482
+ return { success: true, data: result };
483
+ } catch (error) {
484
+ this.onFailure();
485
+ return {
486
+ success: false,
487
+ error: error instanceof Error ? error : new Error(String(error)),
488
+ };
489
+ }
490
+ }
491
+
492
+ private onSuccess(): void {
493
+ this.failures = 0;
494
+ this.state = 'closed';
495
+ }
496
+
497
+ private onFailure(): void {
498
+ this.failures++;
499
+ this.lastFailureTime = Date.now();
500
+
501
+ if (this.failures >= this.options.failureThreshold!) {
502
+ this.state = 'open';
503
+ }
504
+ }
505
+
506
+ getState(): 'closed' | 'open' | 'half-open' {
507
+ return this.state;
508
+ }
509
+
510
+ getFailures(): number {
511
+ return this.failures;
512
+ }
513
+
514
+ reset(): void {
515
+ this.failures = 0;
516
+ this.state = 'closed';
517
+ this.lastFailureTime = 0;
518
+ }
519
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Tests for Byte Formatting Utilities
3
+ */
4
+
5
+ import { describe, expect, it } from 'bun:test';
6
+ import { formatBytes, formatFileSize } from './bytes.js';
7
+
8
+ describe('formatBytes', () => {
9
+ describe('default options (decimals: 2, longUnits)', () => {
10
+ it('formats 0 bytes', () => {
11
+ expect(formatBytes(0)).toBe('0 Bytes');
12
+ });
13
+
14
+ it('formats bytes', () => {
15
+ expect(formatBytes(500)).toBe('500 Bytes');
16
+ });
17
+
18
+ it('formats kilobytes', () => {
19
+ expect(formatBytes(1024)).toBe('1 KB');
20
+ expect(formatBytes(1536)).toBe('1.5 KB');
21
+ });
22
+
23
+ it('formats megabytes', () => {
24
+ expect(formatBytes(1048576)).toBe('1 MB');
25
+ expect(formatBytes(1572864)).toBe('1.5 MB');
26
+ });
27
+
28
+ it('formats gigabytes', () => {
29
+ expect(formatBytes(1073741824)).toBe('1 GB');
30
+ expect(formatBytes(1610612736)).toBe('1.5 GB');
31
+ });
32
+
33
+ it('formats terabytes', () => {
34
+ expect(formatBytes(1099511627776)).toBe('1 TB');
35
+ });
36
+
37
+ it('rounds to 2 decimal places', () => {
38
+ expect(formatBytes(1587)).toBe('1.55 KB');
39
+ expect(formatBytes(1638400)).toBe('1.56 MB');
40
+ });
41
+ });
42
+
43
+ describe('with decimals option', () => {
44
+ it('formats with 0 decimals', () => {
45
+ expect(formatBytes(1536, { decimals: 0 })).toBe('2 KB');
46
+ expect(formatBytes(1024, { decimals: 0 })).toBe('1 KB');
47
+ });
48
+
49
+ it('formats with 1 decimal', () => {
50
+ expect(formatBytes(1536, { decimals: 1 })).toBe('1.5 KB');
51
+ expect(formatBytes(1587, { decimals: 1 })).toBe('1.5 KB');
52
+ });
53
+
54
+ it('formats with 3 decimals', () => {
55
+ expect(formatBytes(1587, { decimals: 3 })).toBe('1.55 KB'); // toFixed trims to 1.550, then trimmed
56
+ });
57
+ });
58
+
59
+ describe('with shortUnits option', () => {
60
+ it('uses short unit for 0 bytes', () => {
61
+ expect(formatBytes(0, { shortUnits: true })).toBe('0 B');
62
+ });
63
+
64
+ it('uses short unit for bytes', () => {
65
+ expect(formatBytes(500, { shortUnits: true })).toBe('500 B');
66
+ });
67
+
68
+ it('uses short units for kilobytes', () => {
69
+ expect(formatBytes(1024, { shortUnits: true })).toBe('1 KB');
70
+ });
71
+
72
+ it('uses short units for megabytes', () => {
73
+ expect(formatBytes(1048576, { shortUnits: true })).toBe('1 MB');
74
+ });
75
+ });
76
+
77
+ describe('combined options', () => {
78
+ it('uses short units with 1 decimal', () => {
79
+ expect(formatBytes(1536, { decimals: 1, shortUnits: true })).toBe('1.5 KB');
80
+ });
81
+
82
+ it('uses short units with 0 decimals', () => {
83
+ expect(formatBytes(1536, { decimals: 0, shortUnits: true })).toBe('2 KB');
84
+ });
85
+ });
86
+
87
+ describe('edge cases', () => {
88
+ it('handles 1 byte', () => {
89
+ expect(formatBytes(1)).toBe('1 Bytes');
90
+ });
91
+
92
+ it('handles very large numbers', () => {
93
+ const result = formatBytes(1099511627776 * 1024); // 1 PB
94
+ expect(result).toContain('TB'); // Will show in TB since we only go up to TB
95
+ });
96
+
97
+ it('handles fractional KB', () => {
98
+ expect(formatBytes(1500)).toBe('1.46 KB');
99
+ });
100
+ });
101
+ });
102
+
103
+ describe('formatFileSize', () => {
104
+ it('formats with 1 decimal and short units', () => {
105
+ expect(formatFileSize(0)).toBe('0 B');
106
+ expect(formatFileSize(1024)).toBe('1 KB'); // toFixed(1) gives "1.0", trimmed to "1"
107
+ expect(formatFileSize(1536)).toBe('1.5 KB');
108
+ expect(formatFileSize(1048576)).toBe('1 MB'); // toFixed(1) gives "1.0", trimmed to "1"
109
+ });
110
+
111
+ it('is an alias for formatBytes with specific options', () => {
112
+ const bytes = 1572864;
113
+ expect(formatFileSize(bytes)).toBe(formatBytes(bytes, { decimals: 1, shortUnits: true }));
114
+ });
115
+ });
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Byte Formatting Utilities
3
+ * Shared utilities for formatting byte sizes with configurable options
4
+ */
5
+
6
+ export interface ByteFormatOptions {
7
+ /** Decimal places to round to (default: 2) */
8
+ decimals?: number;
9
+ /** Use short unit names like 'B', 'KB' instead of 'Bytes', 'KB' (default: false) */
10
+ shortUnits?: boolean;
11
+ }
12
+
13
+ /**
14
+ * Format bytes to human-readable size
15
+ * Pure - number to string formatting
16
+ *
17
+ * @param bytes - The number of bytes to format
18
+ * @param options - Formatting options
19
+ * @returns Formatted string like "1.5 MB" or "2.3 KB"
20
+ *
21
+ * @example
22
+ * formatBytes(0) // '0 Bytes'
23
+ * formatBytes(1024) // '1 KB'
24
+ * formatBytes(1536, { decimals: 1 }) // '1.5 KB'
25
+ * formatBytes(1024, { shortUnits: true }) // '1 KB'
26
+ * formatBytes(0, { shortUnits: true }) // '0 B'
27
+ */
28
+ export function formatBytes(bytes: number, options: ByteFormatOptions = {}): string {
29
+ const { decimals = 2, shortUnits = false } = options;
30
+
31
+ const units = shortUnits
32
+ ? ['B', 'KB', 'MB', 'GB', 'TB']
33
+ : ['Bytes', 'KB', 'MB', 'GB', 'TB'];
34
+
35
+ if (bytes === 0) {
36
+ return `0 ${units[0]}`;
37
+ }
38
+
39
+ const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
40
+ const value = bytes / Math.pow(1024, i);
41
+
42
+ // Format with specified decimal places
43
+ const formatted = value.toFixed(decimals);
44
+
45
+ // Remove trailing zeros and decimal point if not needed
46
+ const trimmed = decimals > 0 ? formatted.replace(/\.?0+$/, '') : formatted;
47
+
48
+ return `${trimmed} ${units[i]}`;
49
+ }
50
+
51
+ /**
52
+ * Format file size - alias for formatBytes with 1 decimal place
53
+ * Kept for backward compatibility
54
+ *
55
+ * @param bytes - The number of bytes to format
56
+ * @returns Formatted string like "1.5 MB"
57
+ *
58
+ * @example
59
+ * formatFileSize(1536) // '1.5 KB'
60
+ * formatFileSize(1048576) // '1.0 MB'
61
+ */
62
+ export function formatFileSize(bytes: number): string {
63
+ return formatBytes(bytes, { decimals: 1, shortUnits: true });
64
+ }