@kb-labs/shared 1.1.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.
Files changed (232) hide show
  1. package/.cursorrules +32 -0
  2. package/.github/workflows/ci.yml +13 -0
  3. package/.github/workflows/deploy.yml +28 -0
  4. package/.github/workflows/docker-build.yml +25 -0
  5. package/.github/workflows/drift-check.yml +10 -0
  6. package/.github/workflows/profiles-validate.yml +16 -0
  7. package/.github/workflows/release.yml +8 -0
  8. package/.kb/devkit/agents/devkit-maintainer/context.globs +15 -0
  9. package/.kb/devkit/agents/devkit-maintainer/permissions.yml +17 -0
  10. package/.kb/devkit/agents/devkit-maintainer/prompt.md +28 -0
  11. package/.kb/devkit/agents/devkit-maintainer/runbook.md +31 -0
  12. package/.kb/devkit/agents/docs-crafter/prompt.md +24 -0
  13. package/.kb/devkit/agents/docs-crafter/runbook.md +18 -0
  14. package/.kb/devkit/agents/release-manager/context.globs +7 -0
  15. package/.kb/devkit/agents/release-manager/prompt.md +27 -0
  16. package/.kb/devkit/agents/release-manager/runbook.md +17 -0
  17. package/.kb/devkit/agents/test-generator/context.globs +7 -0
  18. package/.kb/devkit/agents/test-generator/prompt.md +27 -0
  19. package/.kb/devkit/agents/test-generator/runbook.md +18 -0
  20. package/.vscode/settings.json +23 -0
  21. package/CHANGELOG.md +33 -0
  22. package/CONTRIBUTING.md +117 -0
  23. package/LICENSE +21 -0
  24. package/README.md +306 -0
  25. package/docs/DECLARATIVE-FLAGS-AND-ENV.md +622 -0
  26. package/docs/DOCUMENTATION.md +70 -0
  27. package/docs/adr/0000-template.md +52 -0
  28. package/docs/adr/0001-architecture-and-repository-layout.md +31 -0
  29. package/docs/adr/0002-plugins-and-extensibility.md +44 -0
  30. package/docs/adr/0003-package-and-module-boundaries.md +35 -0
  31. package/docs/adr/0004-versioning-and-release-policy.md +36 -0
  32. package/docs/adr/0005-reactive-loader-pattern.md +179 -0
  33. package/docs/adr/0006-declarative-flags-and-env-systems.md +376 -0
  34. package/eslint.config.js +27 -0
  35. package/kb-labs.config.json +5 -0
  36. package/package.json +88 -0
  37. package/package.json.bin +25 -0
  38. package/package.json.lib +30 -0
  39. package/packages/shared-cli-ui/CHANGELOG.md +20 -0
  40. package/packages/shared-cli-ui/README.md +342 -0
  41. package/packages/shared-cli-ui/docs/ARCHITECTURE.md +105 -0
  42. package/packages/shared-cli-ui/eslint.config.js +27 -0
  43. package/packages/shared-cli-ui/package.json +72 -0
  44. package/packages/shared-cli-ui/src/__tests__/artifacts-display.spec.ts +89 -0
  45. package/packages/shared-cli-ui/src/__tests__/format.spec.ts +44 -0
  46. package/packages/shared-cli-ui/src/__tests__/loader-json-mode.test.ts +119 -0
  47. package/packages/shared-cli-ui/src/artifacts-display.ts +266 -0
  48. package/packages/shared-cli-ui/src/cli-auto-discovery.ts +120 -0
  49. package/packages/shared-cli-ui/src/colors.ts +142 -0
  50. package/packages/shared-cli-ui/src/command-discovery.ts +72 -0
  51. package/packages/shared-cli-ui/src/command-output.ts +153 -0
  52. package/packages/shared-cli-ui/src/command-result.ts +267 -0
  53. package/packages/shared-cli-ui/src/command-runner.ts +310 -0
  54. package/packages/shared-cli-ui/src/command-suggestions.ts +204 -0
  55. package/packages/shared-cli-ui/src/debug/components/output.ts +141 -0
  56. package/packages/shared-cli-ui/src/debug/components/trace.ts +101 -0
  57. package/packages/shared-cli-ui/src/debug/components/tree.ts +88 -0
  58. package/packages/shared-cli-ui/src/debug/formatters/ai.ts +17 -0
  59. package/packages/shared-cli-ui/src/debug/formatters/human.ts +98 -0
  60. package/packages/shared-cli-ui/src/debug/formatters/timeline.ts +94 -0
  61. package/packages/shared-cli-ui/src/debug/index.ts +56 -0
  62. package/packages/shared-cli-ui/src/debug/types.ts +57 -0
  63. package/packages/shared-cli-ui/src/debug/utilities.ts +203 -0
  64. package/packages/shared-cli-ui/src/dynamic-command-discovery.ts +131 -0
  65. package/packages/shared-cli-ui/src/format.ts +412 -0
  66. package/packages/shared-cli-ui/src/index.ts +34 -0
  67. package/packages/shared-cli-ui/src/loader.ts +196 -0
  68. package/packages/shared-cli-ui/src/manifest-parser.ts +151 -0
  69. package/packages/shared-cli-ui/src/modern-format.ts +271 -0
  70. package/packages/shared-cli-ui/src/multi-cli-suggestions.ts +159 -0
  71. package/packages/shared-cli-ui/src/table.ts +134 -0
  72. package/packages/shared-cli-ui/src/timing-tracker.ts +68 -0
  73. package/packages/shared-cli-ui/src/utils/context.ts +12 -0
  74. package/packages/shared-cli-ui/src/utils/env.ts +164 -0
  75. package/packages/shared-cli-ui/src/utils/flags.ts +269 -0
  76. package/packages/shared-cli-ui/src/utils/path.ts +8 -0
  77. package/packages/shared-cli-ui/tsconfig.build.json +15 -0
  78. package/packages/shared-cli-ui/tsconfig.json +9 -0
  79. package/packages/shared-cli-ui/tsup.config.ts +11 -0
  80. package/packages/shared-cli-ui/vitest.config.ts +15 -0
  81. package/packages/shared-command-kit/CHANGELOG.md +20 -0
  82. package/packages/shared-command-kit/LICENSE +22 -0
  83. package/packages/shared-command-kit/README.md +1030 -0
  84. package/packages/shared-command-kit/docs/HIGH-LEVEL-API.md +89 -0
  85. package/packages/shared-command-kit/docs/LOW-LEVEL-API.md +105 -0
  86. package/packages/shared-command-kit/docs/MIGRATION-GUIDE.md +135 -0
  87. package/packages/shared-command-kit/eslint.config.js +27 -0
  88. package/packages/shared-command-kit/eslint.config.ts +14 -0
  89. package/packages/shared-command-kit/package.json +76 -0
  90. package/packages/shared-command-kit/prettierrc.json +5 -0
  91. package/packages/shared-command-kit/src/__tests__/define-command.spec.ts +294 -0
  92. package/packages/shared-command-kit/src/__tests__/define-route.test.ts +285 -0
  93. package/packages/shared-command-kit/src/__tests__/define-system-command.spec.ts +508 -0
  94. package/packages/shared-command-kit/src/__tests__/define-webhook.test.ts +156 -0
  95. package/packages/shared-command-kit/src/__tests__/define-websocket.test.ts +316 -0
  96. package/packages/shared-command-kit/src/__tests__/errors.spec.ts +45 -0
  97. package/packages/shared-command-kit/src/__tests__/flags.spec.ts +353 -0
  98. package/packages/shared-command-kit/src/__tests__/platform-api.test.ts +135 -0
  99. package/packages/shared-command-kit/src/__tests__/plugin-context-v3.snapshot.spec.ts +240 -0
  100. package/packages/shared-command-kit/src/__tests__/ws-types.test.ts +359 -0
  101. package/packages/shared-command-kit/src/analytics/index.ts +6 -0
  102. package/packages/shared-command-kit/src/analytics/with-analytics.ts +195 -0
  103. package/packages/shared-command-kit/src/define-action.ts +100 -0
  104. package/packages/shared-command-kit/src/define-command.ts +113 -0
  105. package/packages/shared-command-kit/src/define-route.ts +113 -0
  106. package/packages/shared-command-kit/src/define-system-command.ts +362 -0
  107. package/packages/shared-command-kit/src/define-webhook.ts +115 -0
  108. package/packages/shared-command-kit/src/define-websocket.ts +308 -0
  109. package/packages/shared-command-kit/src/errors/factory.ts +282 -0
  110. package/packages/shared-command-kit/src/errors/format-validation.ts +144 -0
  111. package/packages/shared-command-kit/src/errors/format.ts +92 -0
  112. package/packages/shared-command-kit/src/errors/index.ts +9 -0
  113. package/packages/shared-command-kit/src/errors/types.ts +32 -0
  114. package/packages/shared-command-kit/src/flags/define.ts +92 -0
  115. package/packages/shared-command-kit/src/flags/index.ts +9 -0
  116. package/packages/shared-command-kit/src/flags/types.ts +153 -0
  117. package/packages/shared-command-kit/src/flags/validate.ts +358 -0
  118. package/packages/shared-command-kit/src/helpers/context.ts +8 -0
  119. package/packages/shared-command-kit/src/helpers/flags.ts +84 -0
  120. package/packages/shared-command-kit/src/helpers/index.ts +42 -0
  121. package/packages/shared-command-kit/src/helpers/patterns.ts +464 -0
  122. package/packages/shared-command-kit/src/helpers/platform.ts +335 -0
  123. package/packages/shared-command-kit/src/helpers/use-analytics.ts +95 -0
  124. package/packages/shared-command-kit/src/helpers/use-cache.ts +97 -0
  125. package/packages/shared-command-kit/src/helpers/use-config.ts +99 -0
  126. package/packages/shared-command-kit/src/helpers/use-embeddings.ts +49 -0
  127. package/packages/shared-command-kit/src/helpers/use-llm.ts +316 -0
  128. package/packages/shared-command-kit/src/helpers/use-logger.ts +77 -0
  129. package/packages/shared-command-kit/src/helpers/use-platform.ts +111 -0
  130. package/packages/shared-command-kit/src/helpers/use-resource-broker.ts +106 -0
  131. package/packages/shared-command-kit/src/helpers/use-storage.ts +71 -0
  132. package/packages/shared-command-kit/src/helpers/use-vector-store.ts +49 -0
  133. package/packages/shared-command-kit/src/helpers/validation.ts +398 -0
  134. package/packages/shared-command-kit/src/index.ts +410 -0
  135. package/packages/shared-command-kit/src/jobs.ts +132 -0
  136. package/packages/shared-command-kit/src/lifecycle/define-handlers.ts +366 -0
  137. package/packages/shared-command-kit/src/lifecycle/index.ts +6 -0
  138. package/packages/shared-command-kit/src/manifest.ts +127 -0
  139. package/packages/shared-command-kit/src/rest/define-handler.ts +187 -0
  140. package/packages/shared-command-kit/src/rest/index.ts +11 -0
  141. package/packages/shared-command-kit/src/studio/index.ts +12 -0
  142. package/packages/shared-command-kit/src/validation/index.ts +6 -0
  143. package/packages/shared-command-kit/src/validation/schema-builders.ts +409 -0
  144. package/packages/shared-command-kit/src/ws-types.ts +106 -0
  145. package/packages/shared-command-kit/tsconfig.build.json +15 -0
  146. package/packages/shared-command-kit/tsconfig.json +9 -0
  147. package/packages/shared-command-kit/tsup.config.ts +30 -0
  148. package/packages/shared-command-kit/vitest.config.ts +4 -0
  149. package/packages/shared-http/package.json +67 -0
  150. package/packages/shared-http/src/__tests__/log-correlation.test.ts +81 -0
  151. package/packages/shared-http/src/__tests__/operation-metrics-tracker.test.ts +55 -0
  152. package/packages/shared-http/src/http-observability-collector.ts +363 -0
  153. package/packages/shared-http/src/index.ts +36 -0
  154. package/packages/shared-http/src/log-correlation.ts +89 -0
  155. package/packages/shared-http/src/operation-metrics-tracker.ts +107 -0
  156. package/packages/shared-http/src/register-openapi.ts +108 -0
  157. package/packages/shared-http/src/resolve-schema-ref.ts +75 -0
  158. package/packages/shared-http/src/schemas.ts +29 -0
  159. package/packages/shared-http/src/service-observability.ts +63 -0
  160. package/packages/shared-http/tsconfig.build.json +15 -0
  161. package/packages/shared-http/tsconfig.json +9 -0
  162. package/packages/shared-http/tsup.config.ts +23 -0
  163. package/packages/shared-http/vitest.config.ts +13 -0
  164. package/packages/shared-perm-presets/CHANGELOG.md +20 -0
  165. package/packages/shared-perm-presets/README.md +78 -0
  166. package/packages/shared-perm-presets/eslint.config.js +27 -0
  167. package/packages/shared-perm-presets/package.json +45 -0
  168. package/packages/shared-perm-presets/src/__tests__/combine.test.ts +403 -0
  169. package/packages/shared-perm-presets/src/__tests__/presets.test.ts +205 -0
  170. package/packages/shared-perm-presets/src/combine.ts +278 -0
  171. package/packages/shared-perm-presets/src/index.ts +18 -0
  172. package/packages/shared-perm-presets/src/presets/ci-environment.ts +34 -0
  173. package/packages/shared-perm-presets/src/presets/full-env.ts +16 -0
  174. package/packages/shared-perm-presets/src/presets/git-workflow.ts +40 -0
  175. package/packages/shared-perm-presets/src/presets/index.ts +8 -0
  176. package/packages/shared-perm-presets/src/presets/kb-platform.ts +30 -0
  177. package/packages/shared-perm-presets/src/presets/llm-access.ts +29 -0
  178. package/packages/shared-perm-presets/src/presets/minimal.ts +21 -0
  179. package/packages/shared-perm-presets/src/presets/npm-publish.ts +48 -0
  180. package/packages/shared-perm-presets/src/presets/vector-store.ts +40 -0
  181. package/packages/shared-perm-presets/src/types.ts +192 -0
  182. package/packages/shared-perm-presets/tsconfig.build.json +15 -0
  183. package/packages/shared-perm-presets/tsconfig.json +9 -0
  184. package/packages/shared-perm-presets/tsup.config.ts +8 -0
  185. package/packages/shared-perm-presets/vitest.config.ts +9 -0
  186. package/packages/shared-testing/CHANGELOG.md +20 -0
  187. package/packages/shared-testing/README.md +430 -0
  188. package/packages/shared-testing/package.json +51 -0
  189. package/packages/shared-testing/src/__tests__/create-test-context.test.ts +199 -0
  190. package/packages/shared-testing/src/__tests__/mock-cache.test.ts +174 -0
  191. package/packages/shared-testing/src/__tests__/mock-llm.test.ts +212 -0
  192. package/packages/shared-testing/src/__tests__/setup-platform.test.ts +90 -0
  193. package/packages/shared-testing/src/__tests__/test-command.test.ts +557 -0
  194. package/packages/shared-testing/src/create-test-context.ts +550 -0
  195. package/packages/shared-testing/src/index.ts +77 -0
  196. package/packages/shared-testing/src/mock-cache.ts +179 -0
  197. package/packages/shared-testing/src/mock-llm.ts +319 -0
  198. package/packages/shared-testing/src/mock-logger.ts +97 -0
  199. package/packages/shared-testing/src/mock-storage.ts +108 -0
  200. package/packages/shared-testing/src/setup-platform.ts +101 -0
  201. package/packages/shared-testing/src/test-command.ts +288 -0
  202. package/packages/shared-testing/tsconfig.build.json +15 -0
  203. package/packages/shared-testing/tsconfig.json +9 -0
  204. package/packages/shared-testing/tsup.config.ts +20 -0
  205. package/packages/shared-testing/vitest.config.ts +3 -0
  206. package/packages/shared-tool-kit/CHANGELOG.md +20 -0
  207. package/packages/shared-tool-kit/package.json +47 -0
  208. package/packages/shared-tool-kit/src/__tests__/factory.test.ts +103 -0
  209. package/packages/shared-tool-kit/src/__tests__/mock-tool.test.ts +95 -0
  210. package/packages/shared-tool-kit/src/factory.ts +126 -0
  211. package/packages/shared-tool-kit/src/index.ts +32 -0
  212. package/packages/shared-tool-kit/src/testing/index.ts +84 -0
  213. package/packages/shared-tool-kit/tsconfig.build.json +15 -0
  214. package/packages/shared-tool-kit/tsconfig.json +9 -0
  215. package/packages/shared-tool-kit/tsup.config.ts +21 -0
  216. package/pnpm-workspace.yaml +11070 -0
  217. package/prettierrc.json +1 -0
  218. package/scripts/devkit-sync.mjs +37 -0
  219. package/scripts/hooks/post-push +9 -0
  220. package/scripts/hooks/pre-commit +9 -0
  221. package/scripts/hooks/pre-push +9 -0
  222. package/tsconfig.base.json +9 -0
  223. package/tsconfig.build.json +15 -0
  224. package/tsconfig.json +9 -0
  225. package/tsconfig.paths.json +50 -0
  226. package/tsconfig.tools.json +18 -0
  227. package/tsup.config.bin.ts +34 -0
  228. package/tsup.config.cli.ts +41 -0
  229. package/tsup.config.dual.ts +46 -0
  230. package/tsup.config.ts +36 -0
  231. package/tsup.external.json +104 -0
  232. package/vitest.config.ts +48 -0
@@ -0,0 +1,464 @@
1
+ /**
2
+ * @module @kb-labs/shared-command-kit/helpers/patterns
3
+ * Common patterns for plugin development (spinner, batch processing, retry logic).
4
+ *
5
+ * These patterns make it easy to build robust, user-friendly plugins with
6
+ * proper error handling and progress feedback.
7
+ */
8
+
9
+ import type { PluginContextV3 as PluginContext } from '@kb-labs/plugin-runtime';
10
+
11
+ // ═══════════════════════════════════════════════════════════════════════════
12
+ // SPINNER PATTERN (Progress feedback)
13
+ // ═══════════════════════════════════════════════════════════════════════════
14
+
15
+ /**
16
+ * Execute function with progress feedback.
17
+ * Shows message while running, updates with success/failure.
18
+ *
19
+ * @param ctx - Plugin context
20
+ * @param message - Progress message (e.g., "Processing files")
21
+ * @param fn - Async function to execute
22
+ * @returns Function result
23
+ * @throws Re-throws any error from fn
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const result = await withSpinner(
28
+ * ctx,
29
+ * 'Indexing documents',
30
+ * async () => {
31
+ * // Long-running operation
32
+ * return await indexDocuments(docs);
33
+ * }
34
+ * );
35
+ * // Output: "Indexing documents... ✓" (or "✗" on error)
36
+ * ```
37
+ */
38
+ export async function withSpinner<T>(
39
+ ctx: PluginContext,
40
+ message: string,
41
+ fn: () => Promise<T>
42
+ ): Promise<T> {
43
+ ctx.ui?.info(`${message}...`);
44
+
45
+ try {
46
+ const result = await fn();
47
+ ctx.ui?.success(`${message} ✓`);
48
+ return result;
49
+ } catch (error) {
50
+ ctx.ui?.error(`${message} ✗`);
51
+ throw error;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Execute multiple steps with progress feedback.
57
+ * Each step shows its own progress message.
58
+ *
59
+ * @param ctx - Plugin context
60
+ * @param steps - Array of steps with message and function
61
+ * @returns Array of step results
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * const [docs, vectors, stored] = await withSteps(ctx, [
66
+ * { message: 'Loading documents', fn: () => loadDocuments() },
67
+ * { message: 'Generating embeddings', fn: () => generateEmbeddings(docs) },
68
+ * { message: 'Storing in database', fn: () => storeVectors(vectors) },
69
+ * ]);
70
+ * ```
71
+ */
72
+ export async function withSteps<T extends any[]>(
73
+ ctx: PluginContext,
74
+ steps: Array<{ message: string; fn: () => Promise<any> }>
75
+ ): Promise<T> {
76
+ const results: any[] = [];
77
+
78
+ for (const step of steps) {
79
+ const result = await withSpinner(ctx, step.message, step.fn);
80
+ results.push(result);
81
+ }
82
+
83
+ return results as T;
84
+ }
85
+
86
+ // ═══════════════════════════════════════════════════════════════════════════
87
+ // BATCH PROCESSING (Concurrency control)
88
+ // ═══════════════════════════════════════════════════════════════════════════
89
+
90
+ export interface BatchOptions<T, R> {
91
+ /** Maximum concurrent operations (default: 5) */
92
+ concurrency?: number;
93
+ /** Progress callback (called after each item) */
94
+ onProgress?: (completed: number, total: number, item: T, result: R) => void;
95
+ /** Error callback (called on item failure, can decide to continue or throw) */
96
+ onError?: (error: Error, item: T) => void | Promise<void>;
97
+ /** Stop on first error (default: false) */
98
+ stopOnError?: boolean;
99
+ }
100
+
101
+ /**
102
+ * Process array of items with concurrency control.
103
+ * Limits number of concurrent operations for better resource management.
104
+ *
105
+ * @param items - Array of items to process
106
+ * @param fn - Processing function
107
+ * @param options - Batch options
108
+ * @returns Array of results
109
+ *
110
+ * @example
111
+ * ```typescript
112
+ * const results = await processBatch(
113
+ * files,
114
+ * async (file) => await processFile(file),
115
+ * {
116
+ * concurrency: 10,
117
+ * onProgress: (completed, total) => {
118
+ * ctx.ui.info(`Processed ${completed}/${total}`);
119
+ * },
120
+ * }
121
+ * );
122
+ * ```
123
+ */
124
+ export async function processBatch<T, R>(
125
+ items: T[],
126
+ fn: (item: T, index: number) => Promise<R>,
127
+ options: BatchOptions<T, R> = {}
128
+ ): Promise<R[]> {
129
+ const { concurrency = 5, onProgress, onError, stopOnError = false } = options;
130
+
131
+ const results: R[] = [];
132
+ const errors: Array<{ index: number; item: T; error: Error }> = [];
133
+ let completed = 0;
134
+
135
+ // Process in batches
136
+ for (let i = 0; i < items.length; i += concurrency) {
137
+ const batch = items.slice(i, i + concurrency);
138
+ const batchPromises = batch.map(async (item, batchIndex) => {
139
+ const index = i + batchIndex;
140
+
141
+ try {
142
+ const result = await fn(item, index);
143
+ results[index] = result;
144
+ completed++;
145
+
146
+ if (onProgress) {
147
+ onProgress(completed, items.length, item, result);
148
+ }
149
+
150
+ return result;
151
+ } catch (error) {
152
+ const err = error instanceof Error ? error : new Error(String(error));
153
+ errors.push({ index, item, error: err });
154
+
155
+ if (onError) {
156
+ await onError(err, item);
157
+ }
158
+
159
+ if (stopOnError) {
160
+ throw err;
161
+ }
162
+
163
+ // Continue processing other items
164
+ return undefined as R;
165
+ }
166
+ });
167
+
168
+ await Promise.all(batchPromises);
169
+ }
170
+
171
+ if (errors.length > 0 && stopOnError) {
172
+ throw new Error(`Batch processing failed: ${errors.length} error(s)`);
173
+ }
174
+
175
+ return results;
176
+ }
177
+
178
+ /**
179
+ * Process items in batches with progress feedback in UI.
180
+ * Convenience wrapper combining processBatch with UI updates.
181
+ *
182
+ * @param ctx - Plugin context
183
+ * @param items - Array of items
184
+ * @param fn - Processing function
185
+ * @param options - Batch options (concurrency, etc.)
186
+ * @returns Array of results
187
+ *
188
+ * @example
189
+ * ```typescript
190
+ * const results = await processBatchWithUI(
191
+ * ctx,
192
+ * files,
193
+ * async (file) => await processFile(file),
194
+ * { concurrency: 10 }
195
+ * );
196
+ * // Automatically shows: "Processing 1/100... 2/100..."
197
+ * ```
198
+ */
199
+ export async function processBatchWithUI<T, R>(
200
+ ctx: PluginContext,
201
+ items: T[],
202
+ fn: (item: T, index: number) => Promise<R>,
203
+ options: Omit<BatchOptions<T, R>, 'onProgress'> = {}
204
+ ): Promise<R[]> {
205
+ return processBatch(items, fn, {
206
+ ...options,
207
+ onProgress: (completed, total) => {
208
+ ctx.ui.info(`Processing ${completed}/${total}...`);
209
+ },
210
+ });
211
+ }
212
+
213
+ // ═══════════════════════════════════════════════════════════════════════════
214
+ // RETRY LOGIC (Error recovery)
215
+ // ═══════════════════════════════════════════════════════════════════════════
216
+
217
+ export interface RetryOptions {
218
+ /** Maximum number of retry attempts (default: 3) */
219
+ maxRetries?: number;
220
+ /** Initial delay in ms (default: 1000) */
221
+ delay?: number;
222
+ /** Backoff multiplier (default: 2 = exponential) */
223
+ backoff?: number;
224
+ /** Maximum delay in ms (default: 30000) */
225
+ maxDelay?: number;
226
+ /** Callback before each retry */
227
+ onRetry?: (attempt: number, error: Error) => void | Promise<void>;
228
+ /** Predicate to determine if error is retryable */
229
+ shouldRetry?: (error: Error) => boolean;
230
+ }
231
+
232
+ /**
233
+ * Retry function with exponential backoff.
234
+ * Useful for transient failures (network, rate limits, etc.).
235
+ *
236
+ * @param fn - Function to retry
237
+ * @param options - Retry options
238
+ * @returns Function result
239
+ * @throws Last error if all retries exhausted
240
+ *
241
+ * @example
242
+ * ```typescript
243
+ * const data = await retryWithBackoff(
244
+ * async () => await fetchFromAPI(),
245
+ * {
246
+ * maxRetries: 5,
247
+ * delay: 1000,
248
+ * backoff: 2,
249
+ * onRetry: (attempt, error) => {
250
+ * console.log(`Retry ${attempt}: ${error.message}`);
251
+ * },
252
+ * }
253
+ * );
254
+ * ```
255
+ */
256
+ export async function retryWithBackoff<T>(
257
+ fn: () => Promise<T>,
258
+ options: RetryOptions = {}
259
+ ): Promise<T> {
260
+ const {
261
+ maxRetries = 3,
262
+ delay = 1000,
263
+ backoff = 2,
264
+ maxDelay = 30000,
265
+ onRetry,
266
+ shouldRetry = () => true,
267
+ } = options;
268
+
269
+ let lastError: Error | undefined;
270
+ let currentDelay = delay;
271
+
272
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
273
+ try {
274
+ return await fn();
275
+ } catch (error) {
276
+ lastError = error instanceof Error ? error : new Error(String(error));
277
+
278
+ // Check if error is retryable
279
+ if (!shouldRetry(lastError)) {
280
+ throw lastError;
281
+ }
282
+
283
+ // Last attempt - don't retry
284
+ if (attempt === maxRetries) {
285
+ throw lastError;
286
+ }
287
+
288
+ // Callback before retry
289
+ if (onRetry) {
290
+ await onRetry(attempt + 1, lastError);
291
+ }
292
+
293
+ // Wait before retry
294
+ await sleep(Math.min(currentDelay, maxDelay));
295
+
296
+ // Increase delay (exponential backoff)
297
+ currentDelay *= backoff;
298
+ }
299
+ }
300
+
301
+ throw lastError instanceof Error ? lastError : new Error('Retry loop exhausted without error');
302
+ }
303
+
304
+ /**
305
+ * Retry with backoff and UI feedback.
306
+ * Shows retry attempts to user.
307
+ *
308
+ * @param ctx - Plugin context
309
+ * @param message - Operation description
310
+ * @param fn - Function to retry
311
+ * @param options - Retry options
312
+ * @returns Function result
313
+ *
314
+ * @example
315
+ * ```typescript
316
+ * const data = await retryWithUI(
317
+ * ctx,
318
+ * 'Fetching data from API',
319
+ * async () => await api.getData(),
320
+ * { maxRetries: 3 }
321
+ * );
322
+ * // Output: "Retrying (1/3)...", "Retrying (2/3)..."
323
+ * ```
324
+ */
325
+ export async function retryWithUI<T>(
326
+ ctx: PluginContext,
327
+ message: string,
328
+ fn: () => Promise<T>,
329
+ options: RetryOptions = {}
330
+ ): Promise<T> {
331
+ return retryWithBackoff(fn, {
332
+ ...options,
333
+ onRetry: async (attempt, error) => {
334
+ ctx.ui?.warn(`${message} failed: ${error.message}`);
335
+ ctx.ui?.info(`Retrying (${attempt}/${options.maxRetries ?? 3})...`);
336
+
337
+ if (options.onRetry) {
338
+ await options.onRetry(attempt, error);
339
+ }
340
+ },
341
+ });
342
+ }
343
+
344
+ // ═══════════════════════════════════════════════════════════════════════════
345
+ // UTILITIES
346
+ // ═══════════════════════════════════════════════════════════════════════════
347
+
348
+ /**
349
+ * Sleep for specified duration.
350
+ *
351
+ * @param ms - Duration in milliseconds
352
+ */
353
+ export function sleep(ms: number): Promise<void> {
354
+ return new Promise<void>((resolve) => { setTimeout(resolve, ms); });
355
+ }
356
+
357
+ /**
358
+ * Timeout wrapper - reject if function takes too long.
359
+ *
360
+ * @param fn - Function to execute
361
+ * @param timeoutMs - Timeout in milliseconds
362
+ * @returns Function result or timeout error
363
+ *
364
+ * @example
365
+ * ```typescript
366
+ * const result = await withTimeout(
367
+ * async () => await slowOperation(),
368
+ * 5000 // 5 seconds
369
+ * );
370
+ * ```
371
+ */
372
+ export async function withTimeout<T>(fn: () => Promise<T>, timeoutMs: number): Promise<T> {
373
+ return Promise.race([
374
+ fn(),
375
+ sleep(timeoutMs).then(() => {
376
+ throw new Error(`Operation timed out after ${timeoutMs}ms`);
377
+ }),
378
+ ]);
379
+ }
380
+
381
+ /**
382
+ * Debounce function - wait for calls to stop before executing.
383
+ * Useful for rate-limited APIs or expensive operations.
384
+ *
385
+ * @param fn - Function to debounce
386
+ * @param delayMs - Delay in milliseconds
387
+ * @returns Debounced function
388
+ *
389
+ * @example
390
+ * ```typescript
391
+ * const debouncedSave = debounce(
392
+ * async (data) => await saveToDatabase(data),
393
+ * 1000
394
+ * );
395
+ *
396
+ * // Only last call will execute
397
+ * debouncedSave(data1);
398
+ * debouncedSave(data2);
399
+ * debouncedSave(data3); // Only this one executes
400
+ * ```
401
+ */
402
+ export function debounce<T extends (...args: any[]) => Promise<any>>(
403
+ fn: T,
404
+ delayMs: number
405
+ ): (...args: Parameters<T>) => Promise<ReturnType<T>> {
406
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
407
+
408
+ return (...args: Parameters<T>): Promise<ReturnType<T>> => {
409
+ return new Promise((resolve, reject) => {
410
+ if (timeoutId) {
411
+ clearTimeout(timeoutId);
412
+ }
413
+
414
+ timeoutId = setTimeout(async () => {
415
+ try {
416
+ const result = await fn(...args);
417
+ resolve(result);
418
+ } catch (error) {
419
+ reject(error);
420
+ }
421
+ }, delayMs);
422
+ });
423
+ };
424
+ }
425
+
426
+ /**
427
+ * Throttle function - execute at most once per interval.
428
+ * Useful for rate-limited APIs.
429
+ *
430
+ * @param fn - Function to throttle
431
+ * @param intervalMs - Minimum interval between calls
432
+ * @returns Throttled function
433
+ *
434
+ * @example
435
+ * ```typescript
436
+ * const throttledLog = throttle(
437
+ * async (message) => await sendToAPI(message),
438
+ * 5000
439
+ * );
440
+ *
441
+ * // Only executes once per 5 seconds
442
+ * throttledLog('msg1'); // Executes immediately
443
+ * throttledLog('msg2'); // Ignored (within 5s)
444
+ * // ... 5s later ...
445
+ * throttledLog('msg3'); // Executes
446
+ * ```
447
+ */
448
+ export function throttle<T extends (...args: any[]) => Promise<any>>(
449
+ fn: T,
450
+ intervalMs: number
451
+ ): (...args: Parameters<T>) => Promise<ReturnType<T> | undefined> {
452
+ let lastCall = 0;
453
+
454
+ return async (...args: Parameters<T>): Promise<ReturnType<T> | undefined> => {
455
+ const now = Date.now();
456
+
457
+ if (now - lastCall >= intervalMs) {
458
+ lastCall = now;
459
+ return fn(...args);
460
+ }
461
+
462
+ return undefined;
463
+ };
464
+ }