@databricks/appkit 0.5.3 → 0.6.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 (207) hide show
  1. package/CLAUDE.md +14 -3
  2. package/NOTICE.md +2 -0
  3. package/bin/appkit.js +0 -0
  4. package/dist/appkit/package.js +1 -1
  5. package/dist/cache/index.js +2 -2
  6. package/dist/cache/index.js.map +1 -1
  7. package/dist/cache/storage/persistent.js.map +1 -1
  8. package/dist/cli/commands/plugins-sync.js +369 -0
  9. package/dist/cli/commands/plugins-sync.js.map +1 -0
  10. package/dist/cli/commands/plugins.js +19 -0
  11. package/dist/cli/commands/plugins.js.map +1 -0
  12. package/dist/cli/index.js +2 -0
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/connectors/index.js +2 -2
  15. package/dist/connectors/{lakebase → lakebase-v1}/client.js +31 -17
  16. package/dist/connectors/lakebase-v1/client.js.map +1 -0
  17. package/dist/connectors/lakebase-v1/defaults.js +18 -0
  18. package/dist/connectors/lakebase-v1/defaults.js.map +1 -0
  19. package/dist/connectors/lakebase-v1/index.js +3 -0
  20. package/dist/core/appkit.d.ts.map +1 -1
  21. package/dist/core/appkit.js +5 -1
  22. package/dist/core/appkit.js.map +1 -1
  23. package/dist/index.d.ts +4 -1
  24. package/dist/index.js +5 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/plugin/plugin.d.ts +95 -3
  27. package/dist/plugin/plugin.d.ts.map +1 -1
  28. package/dist/plugin/plugin.js +94 -6
  29. package/dist/plugin/plugin.js.map +1 -1
  30. package/dist/plugins/analytics/analytics.d.ts +3 -1
  31. package/dist/plugins/analytics/analytics.d.ts.map +1 -1
  32. package/dist/plugins/analytics/analytics.js +3 -1
  33. package/dist/plugins/analytics/analytics.js.map +1 -1
  34. package/dist/plugins/analytics/index.js +1 -0
  35. package/dist/plugins/analytics/manifest.js +21 -0
  36. package/dist/plugins/analytics/manifest.js.map +1 -0
  37. package/dist/plugins/analytics/manifest.json +36 -0
  38. package/dist/plugins/index.js +2 -0
  39. package/dist/plugins/server/index.d.ts +3 -1
  40. package/dist/plugins/server/index.d.ts.map +1 -1
  41. package/dist/plugins/server/index.js +3 -1
  42. package/dist/plugins/server/index.js.map +1 -1
  43. package/dist/plugins/server/manifest.js +21 -0
  44. package/dist/plugins/server/manifest.js.map +1 -0
  45. package/dist/plugins/server/manifest.json +36 -0
  46. package/dist/registry/index.js +5 -0
  47. package/dist/registry/manifest-loader.d.ts +44 -0
  48. package/dist/registry/manifest-loader.d.ts.map +1 -0
  49. package/dist/registry/manifest-loader.js +97 -0
  50. package/dist/registry/manifest-loader.js.map +1 -0
  51. package/dist/registry/resource-registry.d.ts +133 -0
  52. package/dist/registry/resource-registry.d.ts.map +1 -0
  53. package/dist/registry/resource-registry.js +297 -0
  54. package/dist/registry/resource-registry.js.map +1 -0
  55. package/dist/registry/types.d.ts +181 -0
  56. package/dist/registry/types.d.ts.map +1 -0
  57. package/dist/registry/types.js +89 -0
  58. package/dist/registry/types.js.map +1 -0
  59. package/dist/shared/src/plugin.d.ts +66 -1
  60. package/dist/shared/src/plugin.d.ts.map +1 -1
  61. package/dist/type-generator/query-registry.js +4 -1
  62. package/dist/type-generator/query-registry.js.map +1 -1
  63. package/dist/type-generator/spinner.js +3 -0
  64. package/dist/type-generator/spinner.js.map +1 -1
  65. package/dist/type-generator/types.js.map +1 -1
  66. package/docs/docs/api/appkit/Class.AppKitError/index.html +5 -5
  67. package/docs/docs/api/appkit/Class.AuthenticationError/index.html +4 -4
  68. package/docs/docs/api/appkit/Class.ConfigurationError/index.html +4 -4
  69. package/docs/docs/api/appkit/Class.ConnectionError/index.html +4 -4
  70. package/docs/docs/api/appkit/Class.ExecutionError/index.html +4 -4
  71. package/docs/docs/api/appkit/Class.InitializationError/index.html +4 -4
  72. package/docs/docs/api/appkit/Class.Plugin/index.html +28 -16
  73. package/docs/docs/api/appkit/Class.Plugin.md +90 -30
  74. package/docs/docs/api/appkit/Class.ResourceRegistry/index.html +150 -0
  75. package/docs/docs/api/appkit/Class.ResourceRegistry.md +301 -0
  76. package/docs/docs/api/appkit/Class.ServerError/index.html +5 -5
  77. package/docs/docs/api/appkit/Class.TunnelError/index.html +4 -4
  78. package/docs/docs/api/appkit/Class.ValidationError/index.html +4 -4
  79. package/docs/docs/api/appkit/Enumeration.ResourceType/index.html +66 -0
  80. package/docs/docs/api/appkit/Enumeration.ResourceType.md +135 -0
  81. package/docs/docs/api/appkit/Function.appKitTypesPlugin/index.html +4 -4
  82. package/docs/docs/api/appkit/Function.createApp/index.html +4 -4
  83. package/docs/docs/api/appkit/Function.getExecutionContext/index.html +5 -5
  84. package/docs/docs/api/appkit/Function.getPluginManifest/index.html +26 -0
  85. package/docs/docs/api/appkit/Function.getPluginManifest.md +24 -0
  86. package/docs/docs/api/appkit/Function.getResourceRequirements/index.html +28 -0
  87. package/docs/docs/api/appkit/Function.getResourceRequirements.md +42 -0
  88. package/docs/docs/api/appkit/Function.isSQLTypeMarker/index.html +5 -5
  89. package/docs/docs/api/appkit/Interface.BasePluginConfig/index.html +4 -4
  90. package/docs/docs/api/appkit/Interface.CacheConfig/index.html +4 -4
  91. package/docs/docs/api/appkit/Interface.ITelemetry/index.html +5 -5
  92. package/docs/docs/api/appkit/Interface.PluginManifest/index.html +63 -0
  93. package/docs/docs/api/appkit/Interface.PluginManifest.md +135 -0
  94. package/docs/docs/api/appkit/Interface.ResourceEntry/index.html +83 -0
  95. package/docs/docs/api/appkit/Interface.ResourceEntry.md +156 -0
  96. package/docs/docs/api/appkit/Interface.ResourceFieldEntry/index.html +26 -0
  97. package/docs/docs/api/appkit/Interface.ResourceFieldEntry.md +25 -0
  98. package/docs/docs/api/appkit/Interface.ResourceRequirement/index.html +51 -0
  99. package/docs/docs/api/appkit/Interface.ResourceRequirement.md +84 -0
  100. package/docs/docs/api/appkit/Interface.StreamExecutionSettings/index.html +5 -5
  101. package/docs/docs/api/appkit/Interface.TelemetryConfig/index.html +5 -5
  102. package/docs/docs/api/appkit/Interface.ValidationResult/index.html +29 -0
  103. package/docs/docs/api/appkit/Interface.ValidationResult.md +36 -0
  104. package/docs/docs/api/appkit/TypeAlias.ConfigSchema/index.html +21 -0
  105. package/docs/docs/api/appkit/TypeAlias.ConfigSchema.md +12 -0
  106. package/docs/docs/api/appkit/TypeAlias.IAppRouter/index.html +5 -5
  107. package/docs/docs/api/appkit/TypeAlias.ResourcePermission/index.html +18 -0
  108. package/docs/docs/api/appkit/TypeAlias.ResourcePermission.md +20 -0
  109. package/docs/docs/api/appkit/Variable.sql/index.html +5 -5
  110. package/docs/docs/api/appkit/index.html +10 -8
  111. package/docs/docs/api/appkit-ui/data/AreaChart/index.html +3 -3
  112. package/docs/docs/api/appkit-ui/data/BarChart/index.html +3 -3
  113. package/docs/docs/api/appkit-ui/data/DataTable/index.html +3 -3
  114. package/docs/docs/api/appkit-ui/data/DonutChart/index.html +3 -3
  115. package/docs/docs/api/appkit-ui/data/HeatmapChart/index.html +3 -3
  116. package/docs/docs/api/appkit-ui/data/LineChart/index.html +3 -3
  117. package/docs/docs/api/appkit-ui/data/PieChart/index.html +3 -3
  118. package/docs/docs/api/appkit-ui/data/RadarChart/index.html +3 -3
  119. package/docs/docs/api/appkit-ui/data/ScatterChart/index.html +3 -3
  120. package/docs/docs/api/appkit-ui/index.html +3 -3
  121. package/docs/docs/api/appkit-ui/styling/index.html +3 -3
  122. package/docs/docs/api/appkit-ui/ui/Accordion/index.html +3 -3
  123. package/docs/docs/api/appkit-ui/ui/Alert/index.html +3 -3
  124. package/docs/docs/api/appkit-ui/ui/AlertDialog/index.html +3 -3
  125. package/docs/docs/api/appkit-ui/ui/AspectRatio/index.html +3 -3
  126. package/docs/docs/api/appkit-ui/ui/Avatar/index.html +3 -3
  127. package/docs/docs/api/appkit-ui/ui/Badge/index.html +3 -3
  128. package/docs/docs/api/appkit-ui/ui/Breadcrumb/index.html +3 -3
  129. package/docs/docs/api/appkit-ui/ui/Button/index.html +3 -3
  130. package/docs/docs/api/appkit-ui/ui/ButtonGroup/index.html +3 -3
  131. package/docs/docs/api/appkit-ui/ui/Calendar/index.html +3 -3
  132. package/docs/docs/api/appkit-ui/ui/Card/index.html +3 -3
  133. package/docs/docs/api/appkit-ui/ui/Carousel/index.html +3 -3
  134. package/docs/docs/api/appkit-ui/ui/ChartContainer/index.html +3 -3
  135. package/docs/docs/api/appkit-ui/ui/Checkbox/index.html +3 -3
  136. package/docs/docs/api/appkit-ui/ui/Collapsible/index.html +3 -3
  137. package/docs/docs/api/appkit-ui/ui/Command/index.html +3 -3
  138. package/docs/docs/api/appkit-ui/ui/ContextMenu/index.html +3 -3
  139. package/docs/docs/api/appkit-ui/ui/Dialog/index.html +3 -3
  140. package/docs/docs/api/appkit-ui/ui/Drawer/index.html +3 -3
  141. package/docs/docs/api/appkit-ui/ui/DropdownMenu/index.html +3 -3
  142. package/docs/docs/api/appkit-ui/ui/Empty/index.html +3 -3
  143. package/docs/docs/api/appkit-ui/ui/Field/index.html +3 -3
  144. package/docs/docs/api/appkit-ui/ui/FormControl/index.html +3 -3
  145. package/docs/docs/api/appkit-ui/ui/HoverCard/index.html +3 -3
  146. package/docs/docs/api/appkit-ui/ui/Input/index.html +3 -3
  147. package/docs/docs/api/appkit-ui/ui/InputGroup/index.html +3 -3
  148. package/docs/docs/api/appkit-ui/ui/InputOTP/index.html +3 -3
  149. package/docs/docs/api/appkit-ui/ui/Item/index.html +3 -3
  150. package/docs/docs/api/appkit-ui/ui/Kbd/index.html +3 -3
  151. package/docs/docs/api/appkit-ui/ui/Label/index.html +3 -3
  152. package/docs/docs/api/appkit-ui/ui/Menubar/index.html +3 -3
  153. package/docs/docs/api/appkit-ui/ui/NavigationMenu/index.html +3 -3
  154. package/docs/docs/api/appkit-ui/ui/Pagination/index.html +3 -3
  155. package/docs/docs/api/appkit-ui/ui/Popover/index.html +3 -3
  156. package/docs/docs/api/appkit-ui/ui/Progress/index.html +3 -3
  157. package/docs/docs/api/appkit-ui/ui/RadioGroup/index.html +3 -3
  158. package/docs/docs/api/appkit-ui/ui/ResizableHandle/index.html +3 -3
  159. package/docs/docs/api/appkit-ui/ui/ScrollArea/index.html +3 -3
  160. package/docs/docs/api/appkit-ui/ui/Select/index.html +3 -3
  161. package/docs/docs/api/appkit-ui/ui/Separator/index.html +3 -3
  162. package/docs/docs/api/appkit-ui/ui/Sheet/index.html +3 -3
  163. package/docs/docs/api/appkit-ui/ui/Sidebar/index.html +3 -3
  164. package/docs/docs/api/appkit-ui/ui/Skeleton/index.html +3 -3
  165. package/docs/docs/api/appkit-ui/ui/Slider/index.html +3 -3
  166. package/docs/docs/api/appkit-ui/ui/Spinner/index.html +3 -3
  167. package/docs/docs/api/appkit-ui/ui/Switch/index.html +3 -3
  168. package/docs/docs/api/appkit-ui/ui/Table/index.html +3 -3
  169. package/docs/docs/api/appkit-ui/ui/Tabs/index.html +3 -3
  170. package/docs/docs/api/appkit-ui/ui/Textarea/index.html +3 -3
  171. package/docs/docs/api/appkit-ui/ui/Toaster/index.html +3 -3
  172. package/docs/docs/api/appkit-ui/ui/Toggle/index.html +3 -3
  173. package/docs/docs/api/appkit-ui/ui/ToggleGroup/index.html +3 -3
  174. package/docs/docs/api/appkit-ui/ui/Tooltip/index.html +3 -3
  175. package/docs/docs/api/appkit.md +44 -28
  176. package/docs/docs/api/index.html +3 -3
  177. package/docs/docs/app-management/index.html +4 -4
  178. package/docs/docs/app-management.md +1 -1
  179. package/docs/docs/architecture/index.html +4 -4
  180. package/docs/docs/architecture.md +1 -1
  181. package/docs/docs/category/development/index.html +4 -4
  182. package/docs/docs/category/development.md +1 -1
  183. package/docs/docs/configuration/index.html +3 -3
  184. package/docs/docs/core-principles/index.html +3 -3
  185. package/docs/docs/development/ai-assisted-development/index.html +7 -35
  186. package/docs/docs/development/ai-assisted-development.md +3 -52
  187. package/docs/docs/development/index.html +5 -5
  188. package/docs/docs/development/llm-guide/index.html +3 -3
  189. package/docs/docs/development/local-development/index.html +5 -5
  190. package/docs/docs/development/local-development.md +2 -2
  191. package/docs/docs/development/project-setup/index.html +3 -3
  192. package/docs/docs/development/remote-bridge/index.html +4 -4
  193. package/docs/docs/development/remote-bridge.md +1 -1
  194. package/docs/docs/development/type-generation/index.html +3 -3
  195. package/docs/docs/development.md +2 -2
  196. package/docs/docs/index.html +5 -20
  197. package/docs/docs/plugins/index.html +18 -8
  198. package/docs/docs/plugins.md +82 -4
  199. package/docs/docs.md +1 -32
  200. package/llms.txt +14 -3
  201. package/package.json +4 -1
  202. package/dist/connectors/lakebase/client.js.map +0 -1
  203. package/dist/connectors/lakebase/defaults.js +0 -13
  204. package/dist/connectors/lakebase/defaults.js.map +0 -1
  205. package/dist/connectors/lakebase/index.js +0 -3
  206. package/dist/utils/env-validator.js +0 -14
  207. package/dist/utils/env-validator.js.map +0 -1
package/CLAUDE.md CHANGED
@@ -32,20 +32,31 @@ The CLI will display the documentation content directly in the terminal.
32
32
  - [Class: ConnectionError](./docs/docs/api./docs/Class.ConnectionError.md): Error thrown when a connection or network operation fails.
33
33
  - [Class: ExecutionError](./docs/docs/api./docs/Class.ExecutionError.md): Error thrown when an operation execution fails.
34
34
  - [Class: InitializationError](./docs/docs/api./docs/Class.InitializationError.md): Error thrown when a service or component is not properly initialized.
35
- - [Abstract Class: Plugin<TConfig>](./docs/docs/api./docs/Class.Plugin.md): Base abstract class for creating AppKit plugins
35
+ - [Abstract Class: Plugin<TConfig>](./docs/docs/api./docs/Class.Plugin.md): Base abstract class for creating AppKit plugins.
36
+ - [Class: ResourceRegistry](./docs/docs/api./docs/Class.ResourceRegistry.md): Central registry for tracking plugin resource requirements.
36
37
  - [Class: ServerError](./docs/docs/api./docs/Class.ServerError.md): Error thrown when server lifecycle operations fail.
37
38
  - [Class: TunnelError](./docs/docs/api./docs/Class.TunnelError.md): Error thrown when remote tunnel operations fail.
38
39
  - [Class: ValidationError](./docs/docs/api./docs/Class.ValidationError.md): Error thrown when input validation fails.
40
+ - [Enumeration: ResourceType](./docs/docs/api./docs/Enumeration.ResourceType.md): Supported resource types that plugins can depend on.
39
41
  - [Function: appKitTypesPlugin()](./docs/docs/api./docs/Function.appKitTypesPlugin.md): Vite plugin to generate types for AppKit queries.
40
42
  - [Function: createApp()](./docs/docs/api./docs/Function.createApp.md): Bootstraps AppKit with the provided configuration.
41
43
  - [Function: getExecutionContext()](./docs/docs/api./docs/Function.getExecutionContext.md): Get the current execution context.
44
+ - [Function: getPluginManifest()](./docs/docs/api./docs/Function.getPluginManifest.md): Loads and validates the manifest from a plugin constructor.
45
+ - [Function: getResourceRequirements()](./docs/docs/api./docs/Function.getResourceRequirements.md): Gets the resource requirements from a plugin's manifest.
42
46
  - [Function: isSQLTypeMarker()](./docs/docs/api./docs/Function.isSQLTypeMarker.md): Type guard to check if a value is a SQL type marker
43
47
  - [Interface: BasePluginConfig](./docs/docs/api./docs/Interface.BasePluginConfig.md): Base configuration interface for AppKit plugins
44
48
  - [Interface: CacheConfig](./docs/docs/api./docs/Interface.CacheConfig.md): Configuration for caching
45
49
  - [Interface: ITelemetry](./docs/docs/api./docs/Interface.ITelemetry.md): Plugin-facing interface for OpenTelemetry instrumentation.
50
+ - [Interface: PluginManifest](./docs/docs/api./docs/Interface.PluginManifest.md): Plugin manifest that declares metadata and resource requirements.
51
+ - [Interface: ResourceEntry](./docs/docs/api./docs/Interface.ResourceEntry.md): Internal representation of a resource in the registry.
52
+ - [Interface: ResourceFieldEntry](./docs/docs/api./docs/Interface.ResourceFieldEntry.md): Defines a single field for a resource. Each field has its own environment variable and optional description.
53
+ - [Interface: ResourceRequirement](./docs/docs/api./docs/Interface.ResourceRequirement.md): Declares a resource requirement for a plugin.
46
54
  - [Interface: StreamExecutionSettings](./docs/docs/api./docs/Interface.StreamExecutionSettings.md): Configuration for streaming execution with default and user-scoped settings
47
55
  - [Interface: TelemetryConfig](./docs/docs/api./docs/Interface.TelemetryConfig.md): OpenTelemetry configuration for AppKit applications
56
+ - [Interface: ValidationResult](./docs/docs/api./docs/Interface.ValidationResult.md): Result of validating all registered resources against the environment.
57
+ - [Type Alias: ConfigSchema](./docs/docs/api./docs/TypeAlias.ConfigSchema.md): Configuration schema definition for plugin config.
48
58
  - [Type Alias: IAppRouter](./docs/docs/api./docs/TypeAlias.IAppRouter.md): Express router type for plugin route registration
59
+ - [Type Alias: ResourcePermission](./docs/docs/api./docs/TypeAlias.ResourcePermission.md): Union of all possible permission levels across all resource types.
49
60
  - [Variable: sql](./docs/docs/api./docs/Variable.sql.md): SQL helper namespace
50
61
  - [@databricks/appkit-ui](./docs/docs/api/appkit-ui.md): The library provides a set of UI primitives for building Databricks apps in React.
51
62
  - [AreaChart](./docs/docs/api/appkit-ui/data/AreaChart.md): Area Chart component for trend visualization with filled areas.
@@ -117,9 +128,9 @@ The CLI will display the documentation content directly in the terminal.
117
128
  - [Configuration](./docs/docs/configuration.md): This guide covers environment variables and configuration options for AppKit applications.
118
129
  - [Core principles](./docs/docs/core-principles.md): Learn about the fundamental concepts and principles behind AppKit.
119
130
  - [Development](./docs/docs/development.md): AppKit provides multiple development workflows to suit different needs: local development with hot reload, AI-assisted development with MCP, and remote tunneling to deployed backends.
120
- - [AI-Assisted development](./docs/docs/development/ai-assisted-development.md): AppKit integrates with AI coding assistants through the Model Context Protocol (MCP).
131
+ - [AI-Assisted development](./docs/docs/development/ai-assisted-development.md): AppKit-specific agent skills for AI coding assistants are coming soon. This documentation will be updated with instructions when available.
121
132
  - [LLM Guide](./docs/docs/development/llm-guide.md): This document provides prescriptive guidance for AI coding assistants generating code with Databricks AppKit. It is intentionally opinionated to ensure consistent, production-ready code generation.
122
- - [Local development](./docs/docs/development/local-development.md): Once your app is bootstrapped according to the Manual quick start guide, you can start the development server with hot reload for both UI and backend code.
133
+ - [Local development](./docs/docs/development/local-development.md): Once your app is bootstrapped according to the Quick start guide, you can start the development server with hot reload for both UI and backend code.
123
134
  - [Project setup](./docs/docs/development/project-setup.md): This guide covers the recommended project structure and scaffolding for AppKit applications.
124
135
  - [Remote Bridge](./docs/docs/development/remote-bridge.md): Remote bridge allows you to develop against a deployed backend while keeping your UI and queries local. This is useful for testing against production data or debugging deployed backend code without redeploying your app.
125
136
  - [Type generation](./docs/docs/development/type-generation.md): AppKit can automatically generate TypeScript types for your SQL queries, providing end-to-end type safety from database to UI.
package/NOTICE.md CHANGED
@@ -51,6 +51,8 @@ This Software contains code from the following open source projects:
51
51
  | [@tanstack/react-table](https://www.npmjs.com/package/@tanstack/react-table) | 8.21.3 | MIT | https://tanstack.com/table |
52
52
  | [@tanstack/react-virtual](https://www.npmjs.com/package/@tanstack/react-virtual) | 3.13.12 | MIT | https://tanstack.com/virtual |
53
53
  | [@types/semver](https://www.npmjs.com/package/@types/semver) | 7.7.1 | MIT | https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/semver |
54
+ | [ajv](https://www.npmjs.com/package/ajv) | 6.12.6, 8.17.1 | MIT | https://ajv.js.org |
55
+ | [ajv-formats](https://www.npmjs.com/package/ajv-formats) | 2.1.1, 3.0.1 | MIT | https://github.com/ajv-validator/ajv-formats#readme |
54
56
  | [apache-arrow](https://www.npmjs.com/package/apache-arrow) | 21.1.0 | Apache-2.0 | https://arrow.apache.org/js/ |
55
57
  | [class-variance-authority](https://www.npmjs.com/package/class-variance-authority) | 0.7.1 | Apache-2.0 | https://github.com/joe-bell/cva#readme |
56
58
  | [clsx](https://www.npmjs.com/package/clsx) | 2.1.1 | MIT | https://github.com/lukeed/clsx#readme |
package/bin/appkit.js CHANGED
File without changes
@@ -1,6 +1,6 @@
1
1
  //#region package.json
2
2
  var name = "@databricks/appkit";
3
- var version = "0.5.3";
3
+ var version = "0.6.0";
4
4
 
5
5
  //#endregion
6
6
  export { name, version };
@@ -6,7 +6,7 @@ import { ExecutionError } from "../errors/execution.js";
6
6
  import { InitializationError } from "../errors/initialization.js";
7
7
  import { init_errors } from "../errors/index.js";
8
8
  import { deepMerge } from "../utils/merge.js";
9
- import { LakebaseConnector } from "../connectors/lakebase/client.js";
9
+ import { LakebaseV1Connector } from "../connectors/lakebase-v1/client.js";
10
10
  import "../connectors/index.js";
11
11
  import { cacheDefaults } from "./defaults.js";
12
12
  import { InMemoryStorage } from "./storage/memory.js";
@@ -113,7 +113,7 @@ var CacheManager = class CacheManager {
113
113
  return new CacheManager(new InMemoryStorage(config), config);
114
114
  }
115
115
  try {
116
- const connector = new LakebaseConnector({ workspaceClient: new WorkspaceClient({}) });
116
+ const connector = new LakebaseV1Connector({ workspaceClient: new WorkspaceClient({}) });
117
117
  if (await connector.healthCheck()) {
118
118
  const persistentStorage = new PersistentStorage(config, connector);
119
119
  await persistentStorage.initialize();
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/cache/index.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type { CacheConfig, CacheStorage } from \"shared\";\nimport { LakebaseConnector } from \"@/connectors\";\nimport { AppKitError, ExecutionError, InitializationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport type { Counter, TelemetryProvider } from \"../telemetry\";\nimport { SpanStatusCode, TelemetryManager } from \"../telemetry\";\nimport { deepMerge } from \"../utils\";\nimport { cacheDefaults } from \"./defaults\";\nimport { InMemoryStorage, PersistentStorage } from \"./storage\";\n\nconst logger = createLogger(\"cache\");\n\n/**\n * Cache manager class to handle cache operations.\n * Can be used with in-memory storage or persistent storage (Lakebase).\n *\n * The cache is automatically initialized by AppKit. Use `getInstanceSync()` to access\n * the singleton instance after initialization.\n *\n * @internal\n * @example\n * ```typescript\n * const cache = CacheManager.getInstanceSync();\n * const result = await cache.getOrExecute([\"users\", userId], () => fetchUser(userId), userKey);\n * ```\n */\nexport class CacheManager {\n private static readonly MIN_CLEANUP_INTERVAL_MS = 60_000;\n private readonly name: string = \"cache-manager\";\n private static instance: CacheManager | null = null;\n private static initPromise: Promise<CacheManager> | null = null;\n\n private storage: CacheStorage;\n private config: CacheConfig;\n private inFlightRequests: Map<string, Promise<unknown>>;\n private cleanupInProgress: boolean;\n private lastCleanupAttempt: number;\n\n private telemetry: TelemetryProvider;\n private telemetryMetrics: {\n cacheHitCount: Counter;\n cacheMissCount: Counter;\n };\n\n private constructor(storage: CacheStorage, config: CacheConfig) {\n this.storage = storage;\n this.config = config;\n this.inFlightRequests = new Map();\n this.cleanupInProgress = false;\n this.lastCleanupAttempt = 0;\n\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.telemetryMetrics = {\n cacheHitCount: this.telemetry.getMeter().createCounter(\"cache.hit\", {\n description: \"Total number of cache hits\",\n unit: \"1\",\n }),\n cacheMissCount: this.telemetry.getMeter().createCounter(\"cache.miss\", {\n description: \"Total number of cache misses\",\n unit: \"1\",\n }),\n };\n }\n\n /**\n * Get the singleton instance of the cache manager (sync version).\n *\n * Throws if not initialized - ensure AppKit.create() has completed first.\n * @returns CacheManager instance\n */\n static getInstanceSync(): CacheManager {\n if (!CacheManager.instance) {\n throw InitializationError.notInitialized(\n \"CacheManager\",\n \"Ensure AppKit.create() has completed before accessing the cache\",\n );\n }\n\n return CacheManager.instance;\n }\n\n /**\n * Initialize and get the singleton instance of the cache manager.\n * Called internally by AppKit - prefer `getInstanceSync()` for plugin access.\n * @param userConfig - User configuration for the cache manager\n * @returns CacheManager instance\n * @internal\n */\n static async getInstance(\n userConfig?: Partial<CacheConfig>,\n ): Promise<CacheManager> {\n if (CacheManager.instance) {\n return CacheManager.instance;\n }\n\n if (!CacheManager.initPromise) {\n CacheManager.initPromise = CacheManager.create(userConfig).then(\n (instance) => {\n CacheManager.instance = instance;\n return instance;\n },\n );\n }\n\n return CacheManager.initPromise;\n }\n\n /**\n * Create a new cache manager instance\n *\n * Storage selection logic:\n * 1. If `storage` provided and healthy → use provided storage\n * 2. If `storage` provided but unhealthy → fallback to InMemory (or disable if strictPersistence)\n * 3. If no `storage` provided and Lakebase available → use Lakebase\n * 4. If no `storage` provided and Lakebase unavailable → fallback to InMemory (or disable if strictPersistence)\n *\n * @param userConfig - User configuration for the cache manager\n * @returns CacheManager instance\n */\n private static async create(\n userConfig?: Partial<CacheConfig>,\n ): Promise<CacheManager> {\n const config = deepMerge(cacheDefaults, userConfig);\n\n if (config.storage) {\n const isHealthy = await config.storage.healthCheck();\n if (isHealthy) {\n return new CacheManager(config.storage, config);\n }\n\n if (config.strictPersistence) {\n const disabledConfig = { ...config, enabled: false };\n return new CacheManager(\n new InMemoryStorage(disabledConfig),\n disabledConfig,\n );\n }\n\n return new CacheManager(new InMemoryStorage(config), config);\n }\n\n // try to use lakebase storage\n try {\n const workspaceClient = new WorkspaceClient({});\n const connector = new LakebaseConnector({ workspaceClient });\n const isHealthy = await connector.healthCheck();\n\n if (isHealthy) {\n const persistentStorage = new PersistentStorage(config, connector);\n await persistentStorage.initialize();\n return new CacheManager(persistentStorage, config);\n }\n } catch {\n // lakebase unavailable, continue with in-memory storage\n }\n\n if (config.strictPersistence) {\n const disabledConfig = { ...config, enabled: false };\n return new CacheManager(\n new InMemoryStorage(disabledConfig),\n disabledConfig,\n );\n }\n\n return new CacheManager(new InMemoryStorage(config), config);\n }\n\n /**\n * Get or execute a function and cache the result\n * @param key - Cache key\n * @param fn - Function to execute\n * @param userKey - User key\n * @param options - Options for the cache\n * @returns Promise of the result\n */\n async getOrExecute<T>(\n key: (string | number | object)[],\n fn: () => Promise<T>,\n userKey: string,\n options?: { ttl?: number },\n ): Promise<T> {\n if (!this.config.enabled) return fn();\n\n const cacheKey = this.generateKey(key, userKey);\n\n return this.telemetry.startActiveSpan(\n \"cache.getOrExecute\",\n {\n attributes: {\n \"cache.key\": cacheKey,\n \"cache.enabled\": this.config.enabled,\n \"cache.persistent\": this.storage.isPersistent(),\n },\n },\n async (span) => {\n try {\n // check if the value is in the cache\n const cached = await this.storage.get<T>(cacheKey);\n if (cached !== null) {\n span.setAttribute(\"cache.hit\", true);\n span.setStatus({ code: SpanStatusCode.OK });\n this.telemetryMetrics.cacheHitCount.add(1, {\n \"cache.key\": cacheKey,\n });\n\n logger.event()?.setExecution({\n cache_hit: true,\n cache_key: cacheKey,\n });\n\n return cached.value as T;\n }\n\n // check if the value is being processed by another request\n const inFlight = this.inFlightRequests.get(cacheKey);\n if (inFlight) {\n span.setAttribute(\"cache.hit\", true);\n span.setAttribute(\"cache.deduplication\", true);\n span.addEvent(\"cache.deduplication_used\", {\n \"cache.key\": cacheKey,\n });\n span.setStatus({ code: SpanStatusCode.OK });\n this.telemetryMetrics.cacheHitCount.add(1, {\n \"cache.key\": cacheKey,\n \"cache.deduplication\": \"true\",\n });\n\n logger.event()?.setExecution({\n cache_hit: true,\n cache_key: cacheKey,\n cache_deduplication: true,\n });\n\n span.end();\n return inFlight as Promise<T>;\n }\n\n // cache miss - execute function\n span.setAttribute(\"cache.hit\", false);\n span.addEvent(\"cache.miss\", { \"cache.key\": cacheKey });\n this.telemetryMetrics.cacheMissCount.add(1, {\n \"cache.key\": cacheKey,\n });\n\n logger.event()?.setExecution({\n cache_hit: false,\n cache_key: cacheKey,\n });\n\n const promise = fn()\n .then(async (result) => {\n await this.set(cacheKey, result, options);\n span.addEvent(\"cache.value_stored\", {\n \"cache.key\": cacheKey,\n \"cache.ttl\": options?.ttl ?? this.config.ttl ?? 3600,\n });\n return result;\n })\n .catch((error) => {\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n if (error instanceof AppKitError) {\n throw error;\n }\n throw ExecutionError.statementFailed(\n error instanceof Error ? error.message : String(error),\n );\n })\n .finally(() => {\n this.inFlightRequests.delete(cacheKey);\n });\n\n this.inFlightRequests.set(cacheKey, promise);\n\n const result = await promise;\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n } finally {\n span.end();\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n /**\n * Get a cached value\n * @param key - Cache key\n * @returns Promise of the value or null if not found or expired\n */\n async get<T>(key: string): Promise<T | null> {\n if (!this.config.enabled) return null;\n\n // probabilistic cleanup trigger\n this.maybeCleanup();\n\n const entry = await this.storage.get<T>(key);\n if (!entry) return null;\n\n if (Date.now() > entry.expiry) {\n await this.storage.delete(key);\n return null;\n }\n return entry.value as T;\n }\n\n /** Probabilistically trigger cleanup of expired entries (fire-and-forget) */\n private maybeCleanup(): void {\n if (this.cleanupInProgress) return;\n if (!this.storage.isPersistent()) return;\n const now = Date.now();\n if (now - this.lastCleanupAttempt < CacheManager.MIN_CLEANUP_INTERVAL_MS)\n return;\n\n const probability = this.config.cleanupProbability ?? 0.01;\n\n if (Math.random() > probability) return;\n\n this.lastCleanupAttempt = now;\n\n this.cleanupInProgress = true;\n (this.storage as PersistentStorage)\n .cleanupExpired()\n .catch((error) => {\n logger.debug(\"Error cleaning up expired entries: %O\", error);\n })\n .finally(() => {\n this.cleanupInProgress = false;\n });\n }\n\n /**\n * Set a value in the cache\n * @param key - Cache key\n * @param value - Value to set\n * @param options - Options for the cache\n * @returns Promise of the result\n */\n async set<T>(\n key: string,\n value: T,\n options?: { ttl?: number },\n ): Promise<void> {\n if (!this.config.enabled) return;\n\n const ttl = options?.ttl ?? this.config.ttl ?? 3600;\n const expiryTime = Date.now() + ttl * 1000;\n await this.storage.set(key, { value, expiry: expiryTime });\n }\n\n /**\n * Delete a value from the cache\n * @param key - Cache key\n * @returns Promise of the result\n */\n async delete(key: string): Promise<void> {\n if (!this.config.enabled) return;\n await this.storage.delete(key);\n }\n\n /** Clear the cache */\n async clear(): Promise<void> {\n await this.storage.clear();\n this.inFlightRequests.clear();\n }\n\n /**\n * Check if a value exists in the cache\n * @param key - Cache key\n * @returns Promise of true if the value exists, false otherwise\n */\n async has(key: string): Promise<boolean> {\n if (!this.config.enabled) return false;\n\n const entry = await this.storage.get(key);\n if (!entry) return false;\n\n if (Date.now() > entry.expiry) {\n await this.storage.delete(key);\n return false;\n }\n return true;\n }\n\n /**\n * Generate a cache key\n * @param parts - Parts of the key\n * @param userKey - User key\n * @returns Cache key\n */\n generateKey(parts: (string | number | object)[], userKey: string): string {\n const allParts = [userKey, ...parts];\n const serialized = JSON.stringify(allParts);\n return createHash(\"sha256\").update(serialized).digest(\"hex\");\n }\n\n /** Close the cache */\n async close(): Promise<void> {\n await this.storage.close();\n }\n\n /**\n * Check if the storage is healthy\n * @returns Promise of true if the storage is healthy, false otherwise\n */\n async isStorageHealthy(): Promise<boolean> {\n return this.storage.healthCheck();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/cache/index.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type { CacheConfig, CacheStorage } from \"shared\";\nimport { LakebaseV1Connector } from \"@/connectors\";\nimport { AppKitError, ExecutionError, InitializationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport type { Counter, TelemetryProvider } from \"../telemetry\";\nimport { SpanStatusCode, TelemetryManager } from \"../telemetry\";\nimport { deepMerge } from \"../utils\";\nimport { cacheDefaults } from \"./defaults\";\nimport { InMemoryStorage, PersistentStorage } from \"./storage\";\n\nconst logger = createLogger(\"cache\");\n\n/**\n * Cache manager class to handle cache operations.\n * Can be used with in-memory storage or persistent storage (Lakebase).\n *\n * The cache is automatically initialized by AppKit. Use `getInstanceSync()` to access\n * the singleton instance after initialization.\n *\n * @internal\n * @example\n * ```typescript\n * const cache = CacheManager.getInstanceSync();\n * const result = await cache.getOrExecute([\"users\", userId], () => fetchUser(userId), userKey);\n * ```\n */\nexport class CacheManager {\n private static readonly MIN_CLEANUP_INTERVAL_MS = 60_000;\n private readonly name: string = \"cache-manager\";\n private static instance: CacheManager | null = null;\n private static initPromise: Promise<CacheManager> | null = null;\n\n private storage: CacheStorage;\n private config: CacheConfig;\n private inFlightRequests: Map<string, Promise<unknown>>;\n private cleanupInProgress: boolean;\n private lastCleanupAttempt: number;\n\n private telemetry: TelemetryProvider;\n private telemetryMetrics: {\n cacheHitCount: Counter;\n cacheMissCount: Counter;\n };\n\n private constructor(storage: CacheStorage, config: CacheConfig) {\n this.storage = storage;\n this.config = config;\n this.inFlightRequests = new Map();\n this.cleanupInProgress = false;\n this.lastCleanupAttempt = 0;\n\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.telemetryMetrics = {\n cacheHitCount: this.telemetry.getMeter().createCounter(\"cache.hit\", {\n description: \"Total number of cache hits\",\n unit: \"1\",\n }),\n cacheMissCount: this.telemetry.getMeter().createCounter(\"cache.miss\", {\n description: \"Total number of cache misses\",\n unit: \"1\",\n }),\n };\n }\n\n /**\n * Get the singleton instance of the cache manager (sync version).\n *\n * Throws if not initialized - ensure AppKit.create() has completed first.\n * @returns CacheManager instance\n */\n static getInstanceSync(): CacheManager {\n if (!CacheManager.instance) {\n throw InitializationError.notInitialized(\n \"CacheManager\",\n \"Ensure AppKit.create() has completed before accessing the cache\",\n );\n }\n\n return CacheManager.instance;\n }\n\n /**\n * Initialize and get the singleton instance of the cache manager.\n * Called internally by AppKit - prefer `getInstanceSync()` for plugin access.\n * @param userConfig - User configuration for the cache manager\n * @returns CacheManager instance\n * @internal\n */\n static async getInstance(\n userConfig?: Partial<CacheConfig>,\n ): Promise<CacheManager> {\n if (CacheManager.instance) {\n return CacheManager.instance;\n }\n\n if (!CacheManager.initPromise) {\n CacheManager.initPromise = CacheManager.create(userConfig).then(\n (instance) => {\n CacheManager.instance = instance;\n return instance;\n },\n );\n }\n\n return CacheManager.initPromise;\n }\n\n /**\n * Create a new cache manager instance\n *\n * Storage selection logic:\n * 1. If `storage` provided and healthy → use provided storage\n * 2. If `storage` provided but unhealthy → fallback to InMemory (or disable if strictPersistence)\n * 3. If no `storage` provided and Lakebase available → use Lakebase\n * 4. If no `storage` provided and Lakebase unavailable → fallback to InMemory (or disable if strictPersistence)\n *\n * @param userConfig - User configuration for the cache manager\n * @returns CacheManager instance\n */\n private static async create(\n userConfig?: Partial<CacheConfig>,\n ): Promise<CacheManager> {\n const config = deepMerge(cacheDefaults, userConfig);\n\n if (config.storage) {\n const isHealthy = await config.storage.healthCheck();\n if (isHealthy) {\n return new CacheManager(config.storage, config);\n }\n\n if (config.strictPersistence) {\n const disabledConfig = { ...config, enabled: false };\n return new CacheManager(\n new InMemoryStorage(disabledConfig),\n disabledConfig,\n );\n }\n\n return new CacheManager(new InMemoryStorage(config), config);\n }\n\n // try to use lakebase storage\n try {\n const workspaceClient = new WorkspaceClient({});\n const connector = new LakebaseV1Connector({ workspaceClient });\n const isHealthy = await connector.healthCheck();\n\n if (isHealthy) {\n const persistentStorage = new PersistentStorage(config, connector);\n await persistentStorage.initialize();\n return new CacheManager(persistentStorage, config);\n }\n } catch {\n // lakebase unavailable, continue with in-memory storage\n }\n\n if (config.strictPersistence) {\n const disabledConfig = { ...config, enabled: false };\n return new CacheManager(\n new InMemoryStorage(disabledConfig),\n disabledConfig,\n );\n }\n\n return new CacheManager(new InMemoryStorage(config), config);\n }\n\n /**\n * Get or execute a function and cache the result\n * @param key - Cache key\n * @param fn - Function to execute\n * @param userKey - User key\n * @param options - Options for the cache\n * @returns Promise of the result\n */\n async getOrExecute<T>(\n key: (string | number | object)[],\n fn: () => Promise<T>,\n userKey: string,\n options?: { ttl?: number },\n ): Promise<T> {\n if (!this.config.enabled) return fn();\n\n const cacheKey = this.generateKey(key, userKey);\n\n return this.telemetry.startActiveSpan(\n \"cache.getOrExecute\",\n {\n attributes: {\n \"cache.key\": cacheKey,\n \"cache.enabled\": this.config.enabled,\n \"cache.persistent\": this.storage.isPersistent(),\n },\n },\n async (span) => {\n try {\n // check if the value is in the cache\n const cached = await this.storage.get<T>(cacheKey);\n if (cached !== null) {\n span.setAttribute(\"cache.hit\", true);\n span.setStatus({ code: SpanStatusCode.OK });\n this.telemetryMetrics.cacheHitCount.add(1, {\n \"cache.key\": cacheKey,\n });\n\n logger.event()?.setExecution({\n cache_hit: true,\n cache_key: cacheKey,\n });\n\n return cached.value as T;\n }\n\n // check if the value is being processed by another request\n const inFlight = this.inFlightRequests.get(cacheKey);\n if (inFlight) {\n span.setAttribute(\"cache.hit\", true);\n span.setAttribute(\"cache.deduplication\", true);\n span.addEvent(\"cache.deduplication_used\", {\n \"cache.key\": cacheKey,\n });\n span.setStatus({ code: SpanStatusCode.OK });\n this.telemetryMetrics.cacheHitCount.add(1, {\n \"cache.key\": cacheKey,\n \"cache.deduplication\": \"true\",\n });\n\n logger.event()?.setExecution({\n cache_hit: true,\n cache_key: cacheKey,\n cache_deduplication: true,\n });\n\n span.end();\n return inFlight as Promise<T>;\n }\n\n // cache miss - execute function\n span.setAttribute(\"cache.hit\", false);\n span.addEvent(\"cache.miss\", { \"cache.key\": cacheKey });\n this.telemetryMetrics.cacheMissCount.add(1, {\n \"cache.key\": cacheKey,\n });\n\n logger.event()?.setExecution({\n cache_hit: false,\n cache_key: cacheKey,\n });\n\n const promise = fn()\n .then(async (result) => {\n await this.set(cacheKey, result, options);\n span.addEvent(\"cache.value_stored\", {\n \"cache.key\": cacheKey,\n \"cache.ttl\": options?.ttl ?? this.config.ttl ?? 3600,\n });\n return result;\n })\n .catch((error) => {\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n if (error instanceof AppKitError) {\n throw error;\n }\n throw ExecutionError.statementFailed(\n error instanceof Error ? error.message : String(error),\n );\n })\n .finally(() => {\n this.inFlightRequests.delete(cacheKey);\n });\n\n this.inFlightRequests.set(cacheKey, promise);\n\n const result = await promise;\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n } finally {\n span.end();\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n /**\n * Get a cached value\n * @param key - Cache key\n * @returns Promise of the value or null if not found or expired\n */\n async get<T>(key: string): Promise<T | null> {\n if (!this.config.enabled) return null;\n\n // probabilistic cleanup trigger\n this.maybeCleanup();\n\n const entry = await this.storage.get<T>(key);\n if (!entry) return null;\n\n if (Date.now() > entry.expiry) {\n await this.storage.delete(key);\n return null;\n }\n return entry.value as T;\n }\n\n /** Probabilistically trigger cleanup of expired entries (fire-and-forget) */\n private maybeCleanup(): void {\n if (this.cleanupInProgress) return;\n if (!this.storage.isPersistent()) return;\n const now = Date.now();\n if (now - this.lastCleanupAttempt < CacheManager.MIN_CLEANUP_INTERVAL_MS)\n return;\n\n const probability = this.config.cleanupProbability ?? 0.01;\n\n if (Math.random() > probability) return;\n\n this.lastCleanupAttempt = now;\n\n this.cleanupInProgress = true;\n (this.storage as PersistentStorage)\n .cleanupExpired()\n .catch((error) => {\n logger.debug(\"Error cleaning up expired entries: %O\", error);\n })\n .finally(() => {\n this.cleanupInProgress = false;\n });\n }\n\n /**\n * Set a value in the cache\n * @param key - Cache key\n * @param value - Value to set\n * @param options - Options for the cache\n * @returns Promise of the result\n */\n async set<T>(\n key: string,\n value: T,\n options?: { ttl?: number },\n ): Promise<void> {\n if (!this.config.enabled) return;\n\n const ttl = options?.ttl ?? this.config.ttl ?? 3600;\n const expiryTime = Date.now() + ttl * 1000;\n await this.storage.set(key, { value, expiry: expiryTime });\n }\n\n /**\n * Delete a value from the cache\n * @param key - Cache key\n * @returns Promise of the result\n */\n async delete(key: string): Promise<void> {\n if (!this.config.enabled) return;\n await this.storage.delete(key);\n }\n\n /** Clear the cache */\n async clear(): Promise<void> {\n await this.storage.clear();\n this.inFlightRequests.clear();\n }\n\n /**\n * Check if a value exists in the cache\n * @param key - Cache key\n * @returns Promise of true if the value exists, false otherwise\n */\n async has(key: string): Promise<boolean> {\n if (!this.config.enabled) return false;\n\n const entry = await this.storage.get(key);\n if (!entry) return false;\n\n if (Date.now() > entry.expiry) {\n await this.storage.delete(key);\n return false;\n }\n return true;\n }\n\n /**\n * Generate a cache key\n * @param parts - Parts of the key\n * @param userKey - User key\n * @returns Cache key\n */\n generateKey(parts: (string | number | object)[], userKey: string): string {\n const allParts = [userKey, ...parts];\n const serialized = JSON.stringify(allParts);\n return createHash(\"sha256\").update(serialized).digest(\"hex\");\n }\n\n /** Close the cache */\n async close(): Promise<void> {\n await this.storage.close();\n }\n\n /**\n * Check if the storage is healthy\n * @returns Promise of true if the storage is healthy, false otherwise\n */\n async isStorageHealthy(): Promise<boolean> {\n return this.storage.healthCheck();\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"persistent.js","names":[],"sources":["../../../src/cache/storage/persistent.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\nimport type { LakebaseConnector } from \"../../connectors\";\nimport { InitializationError, ValidationError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { lakebaseStorageDefaults } from \"./defaults\";\n\nconst logger = createLogger(\"cache:persistent\");\n\n/**\n * Persistent cache storage implementation. Uses a least recently used (LRU) eviction policy\n * to manage memory usage and ensure efficient cache operations.\n *\n * @example\n * const persistentStorage = new PersistentStorage(config, connector);\n * await persistentStorage.initialize();\n * await persistentStorage.get(\"my-key\");\n * await persistentStorage.set(\"my-key\", \"my-value\");\n * await persistentStorage.delete(\"my-key\");\n * await persistentStorage.clear();\n * await persistentStorage.has(\"my-key\");\n *\n */\nexport class PersistentStorage implements CacheStorage {\n private readonly connector: LakebaseConnector;\n private readonly tableName: string;\n private readonly maxBytes: number;\n private readonly maxEntryBytes: number;\n private readonly evictionBatchSize: number;\n private readonly evictionCheckProbability: number;\n private initialized: boolean;\n\n constructor(config: CacheConfig, connector: LakebaseConnector) {\n this.connector = connector;\n this.maxBytes = config.maxBytes ?? lakebaseStorageDefaults.maxBytes;\n this.maxEntryBytes =\n config.maxEntryBytes ?? lakebaseStorageDefaults.maxEntryBytes;\n this.evictionBatchSize = lakebaseStorageDefaults.evictionBatchSize;\n this.evictionCheckProbability =\n config.evictionCheckProbability ??\n lakebaseStorageDefaults.evictionCheckProbability;\n this.tableName = lakebaseStorageDefaults.tableName; // hardcoded, safe for now\n this.initialized = false;\n }\n\n /** Initialize the persistent storage and run migrations if necessary */\n async initialize(): Promise<void> {\n if (this.initialized) return;\n\n try {\n await this.runMigrations();\n this.initialized = true;\n } catch (error) {\n logger.error(\"Error in persistent storage initialization: %O\", error);\n throw error;\n }\n }\n\n /**\n * Get a cached value from the persistent storage\n * @param key - Cache key\n * @returns Promise of the cached value or null if not found\n */\n async get<T>(key: string): Promise<CacheEntry<T> | null> {\n await this.ensureInitialized();\n\n const keyHash = this.hashKey(key);\n\n const result = await this.connector.query<{\n value: Buffer;\n expiry: string;\n }>(`SELECT value, expiry FROM ${this.tableName} WHERE key_hash = $1`, [\n keyHash,\n ]);\n\n if (result.rows.length === 0) return null;\n\n const entry = result.rows[0];\n\n // fire-and-forget update\n this.connector\n .query(\n `UPDATE ${this.tableName} SET last_accessed = NOW() WHERE key_hash = $1`,\n [keyHash],\n )\n .catch(() => {\n logger.debug(\"Error updating last_accessed time for key: %s\", key);\n });\n\n return {\n value: this.deserializeValue<T>(entry.value),\n expiry: Number(entry.expiry),\n };\n }\n\n /**\n * Set a value in the persistent storage\n * @param key - Cache key\n * @param entry - Cache entry\n * @returns Promise of the result\n */\n async set<T>(key: string, entry: CacheEntry<T>): Promise<void> {\n await this.ensureInitialized();\n\n const keyHash = this.hashKey(key);\n const keyBytes = Buffer.from(key, \"utf-8\");\n const valueBytes = this.serializeValue(entry.value);\n const byteSize = keyBytes.length + valueBytes.length;\n\n if (byteSize > this.maxEntryBytes) {\n throw ValidationError.invalidValue(\n \"cache entry size\",\n byteSize,\n `maximum ${this.maxEntryBytes} bytes`,\n );\n }\n\n // probabilistic eviction check\n if (Math.random() < this.evictionCheckProbability) {\n const totalBytes = await this.totalBytes();\n if (totalBytes + byteSize > this.maxBytes) {\n await this.evictBySize(byteSize);\n }\n }\n\n await this.connector.query(\n `INSERT INTO ${this.tableName} (key_hash, key, value, byte_size, expiry, created_at, last_accessed)\n VALUES ($1, $2, $3, $4, $5, NOW(), NOW())\n ON CONFLICT (key_hash)\n DO UPDATE SET value = $3, byte_size = $4, expiry = $5, last_accessed = NOW()\n `,\n [keyHash, keyBytes, valueBytes, byteSize, entry.expiry],\n );\n }\n\n /**\n * Delete a value from the persistent storage\n * @param key - Cache key\n * @returns Promise of the result\n */\n async delete(key: string): Promise<void> {\n await this.ensureInitialized();\n const keyHash = this.hashKey(key);\n await this.connector.query(\n `DELETE FROM ${this.tableName} WHERE key_hash = $1`,\n [keyHash],\n );\n }\n\n /** Clear the persistent storage */\n async clear(): Promise<void> {\n await this.ensureInitialized();\n await this.connector.query(`TRUNCATE TABLE ${this.tableName}`);\n }\n\n /**\n * Check if a value exists in the persistent storage\n * @param key - Cache key\n * @returns Promise of true if the value exists, false otherwise\n */\n async has(key: string): Promise<boolean> {\n await this.ensureInitialized();\n const keyHash = this.hashKey(key);\n\n const result = await this.connector.query<{ exists: boolean }>(\n `SELECT EXISTS(SELECT 1 FROM ${this.tableName} WHERE key_hash = $1) as exists`,\n [keyHash],\n );\n\n return result.rows[0]?.exists ?? false;\n }\n\n /**\n * Get the size of the persistent storage\n * @returns Promise of the size of the storage\n */\n async size(): Promise<number> {\n await this.ensureInitialized();\n\n const result = await this.connector.query<{ count: string }>(\n `SELECT COUNT(*) as count FROM ${this.tableName}`,\n );\n return parseInt(result.rows[0]?.count ?? \"0\", 10);\n }\n\n /** Get the total number of bytes in the persistent storage */\n async totalBytes(): Promise<number> {\n await this.ensureInitialized();\n\n const result = await this.connector.query<{ total: string }>(\n `SELECT COALESCE(SUM(byte_size), 0) as total FROM ${this.tableName}`,\n );\n return parseInt(result.rows[0]?.total ?? \"0\", 10);\n }\n\n /**\n * Check if the persistent storage is persistent\n * @returns true if the storage is persistent, false otherwise\n */\n isPersistent(): boolean {\n return true;\n }\n\n /**\n * Check if the persistent storage is healthy\n * @returns Promise of true if the storage is healthy, false otherwise\n */\n async healthCheck(): Promise<boolean> {\n try {\n return await this.connector.healthCheck();\n } catch {\n return false;\n }\n }\n\n /** Close the persistent storage */\n async close(): Promise<void> {\n await this.connector.close();\n }\n\n /**\n * Cleanup expired entries from the persistent storage\n * @returns Promise of the number of expired entries\n */\n async cleanupExpired(): Promise<number> {\n await this.ensureInitialized();\n const result = await this.connector.query<{ count: string }>(\n `WITH deleted as (DELETE FROM ${this.tableName} WHERE expiry < $1 RETURNING *) SELECT COUNT(*) as count FROM deleted`,\n [Date.now()],\n );\n return parseInt(result.rows[0]?.count ?? \"0\", 10);\n }\n\n /** Evict entries from the persistent storage by size */\n private async evictBySize(requiredBytes: number): Promise<void> {\n const freedByExpiry = await this.cleanupExpired();\n if (freedByExpiry > 0) {\n const currentBytes = await this.totalBytes();\n if (currentBytes + requiredBytes <= this.maxBytes) {\n return;\n }\n }\n\n await this.connector.query(\n `DELETE FROM ${this.tableName} WHERE key_hash IN\n (SELECT key_hash FROM ${this.tableName} ORDER BY last_accessed ASC LIMIT $1)`,\n [this.evictionBatchSize],\n );\n }\n\n /** Ensure the persistent storage is initialized */\n private async ensureInitialized(): Promise<void> {\n if (!this.initialized) {\n await this.initialize();\n }\n }\n\n /** Generate a 64-bit hash for the cache key using SHA256 */\n private hashKey(key: string): bigint {\n if (!key) throw ValidationError.missingField(\"key\");\n const hash = createHash(\"sha256\").update(key).digest();\n return hash.readBigInt64BE(0);\n }\n\n /** Serialize a value to a buffer */\n private serializeValue<T>(value: T): Buffer {\n return Buffer.from(JSON.stringify(value), \"utf-8\");\n }\n\n /** Deserialize a value from a buffer */\n private deserializeValue<T>(buffer: Buffer): T {\n return JSON.parse(buffer.toString(\"utf-8\")) as T;\n }\n\n /** Run migrations for the persistent storage */\n private async runMigrations(): Promise<void> {\n try {\n await this.connector.query(`\n CREATE TABLE IF NOT EXISTS ${this.tableName} (\n id BIGSERIAL PRIMARY KEY,\n key_hash BIGINT NOT NULL,\n key BYTEA NOT NULL,\n value BYTEA NOT NULL,\n byte_size INTEGER NOT NULL,\n expiry BIGINT NOT NULL,\n created_at TIMESTAMP NOT NULL DEFAULT NOW(),\n last_accessed TIMESTAMP NOT NULL DEFAULT NOW()\n )\n `);\n\n // unique index on key_hash for fast lookups\n await this.connector.query(\n `CREATE UNIQUE INDEX IF NOT EXISTS idx_${this.tableName}_key_hash ON ${this.tableName} (key_hash);`,\n );\n\n // index on expiry for cleanup queries\n await this.connector.query(\n `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_expiry ON ${this.tableName} (expiry); `,\n );\n\n // index on last_accessed for LRU eviction\n await this.connector.query(\n `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_last_accessed ON ${this.tableName} (last_accessed); `,\n );\n\n // index on byte_size for monitoring\n await this.connector.query(\n `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_byte_size ON ${this.tableName} (byte_size); `,\n );\n } catch (error) {\n logger.error(\n \"Error in running migrations for persistent storage: %O\",\n error,\n );\n throw InitializationError.migrationFailed(error as Error);\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"persistent.js","names":[],"sources":["../../../src/cache/storage/persistent.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\nimport type { LakebaseV1Connector } from \"../../connectors\";\nimport { InitializationError, ValidationError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { lakebaseStorageDefaults } from \"./defaults\";\n\nconst logger = createLogger(\"cache:persistent\");\n\n/**\n * Persistent cache storage implementation. Uses a least recently used (LRU) eviction policy\n * to manage memory usage and ensure efficient cache operations.\n *\n * @example\n * const persistentStorage = new PersistentStorage(config, connector);\n * await persistentStorage.initialize();\n * await persistentStorage.get(\"my-key\");\n * await persistentStorage.set(\"my-key\", \"my-value\");\n * await persistentStorage.delete(\"my-key\");\n * await persistentStorage.clear();\n * await persistentStorage.has(\"my-key\");\n *\n */\nexport class PersistentStorage implements CacheStorage {\n private readonly connector: LakebaseV1Connector;\n private readonly tableName: string;\n private readonly maxBytes: number;\n private readonly maxEntryBytes: number;\n private readonly evictionBatchSize: number;\n private readonly evictionCheckProbability: number;\n private initialized: boolean;\n\n constructor(config: CacheConfig, connector: LakebaseV1Connector) {\n this.connector = connector;\n this.maxBytes = config.maxBytes ?? lakebaseStorageDefaults.maxBytes;\n this.maxEntryBytes =\n config.maxEntryBytes ?? lakebaseStorageDefaults.maxEntryBytes;\n this.evictionBatchSize = lakebaseStorageDefaults.evictionBatchSize;\n this.evictionCheckProbability =\n config.evictionCheckProbability ??\n lakebaseStorageDefaults.evictionCheckProbability;\n this.tableName = lakebaseStorageDefaults.tableName; // hardcoded, safe for now\n this.initialized = false;\n }\n\n /** Initialize the persistent storage and run migrations if necessary */\n async initialize(): Promise<void> {\n if (this.initialized) return;\n\n try {\n await this.runMigrations();\n this.initialized = true;\n } catch (error) {\n logger.error(\"Error in persistent storage initialization: %O\", error);\n throw error;\n }\n }\n\n /**\n * Get a cached value from the persistent storage\n * @param key - Cache key\n * @returns Promise of the cached value or null if not found\n */\n async get<T>(key: string): Promise<CacheEntry<T> | null> {\n await this.ensureInitialized();\n\n const keyHash = this.hashKey(key);\n\n const result = await this.connector.query<{\n value: Buffer;\n expiry: string;\n }>(`SELECT value, expiry FROM ${this.tableName} WHERE key_hash = $1`, [\n keyHash,\n ]);\n\n if (result.rows.length === 0) return null;\n\n const entry = result.rows[0];\n\n // fire-and-forget update\n this.connector\n .query(\n `UPDATE ${this.tableName} SET last_accessed = NOW() WHERE key_hash = $1`,\n [keyHash],\n )\n .catch(() => {\n logger.debug(\"Error updating last_accessed time for key: %s\", key);\n });\n\n return {\n value: this.deserializeValue<T>(entry.value),\n expiry: Number(entry.expiry),\n };\n }\n\n /**\n * Set a value in the persistent storage\n * @param key - Cache key\n * @param entry - Cache entry\n * @returns Promise of the result\n */\n async set<T>(key: string, entry: CacheEntry<T>): Promise<void> {\n await this.ensureInitialized();\n\n const keyHash = this.hashKey(key);\n const keyBytes = Buffer.from(key, \"utf-8\");\n const valueBytes = this.serializeValue(entry.value);\n const byteSize = keyBytes.length + valueBytes.length;\n\n if (byteSize > this.maxEntryBytes) {\n throw ValidationError.invalidValue(\n \"cache entry size\",\n byteSize,\n `maximum ${this.maxEntryBytes} bytes`,\n );\n }\n\n // probabilistic eviction check\n if (Math.random() < this.evictionCheckProbability) {\n const totalBytes = await this.totalBytes();\n if (totalBytes + byteSize > this.maxBytes) {\n await this.evictBySize(byteSize);\n }\n }\n\n await this.connector.query(\n `INSERT INTO ${this.tableName} (key_hash, key, value, byte_size, expiry, created_at, last_accessed)\n VALUES ($1, $2, $3, $4, $5, NOW(), NOW())\n ON CONFLICT (key_hash)\n DO UPDATE SET value = $3, byte_size = $4, expiry = $5, last_accessed = NOW()\n `,\n [keyHash, keyBytes, valueBytes, byteSize, entry.expiry],\n );\n }\n\n /**\n * Delete a value from the persistent storage\n * @param key - Cache key\n * @returns Promise of the result\n */\n async delete(key: string): Promise<void> {\n await this.ensureInitialized();\n const keyHash = this.hashKey(key);\n await this.connector.query(\n `DELETE FROM ${this.tableName} WHERE key_hash = $1`,\n [keyHash],\n );\n }\n\n /** Clear the persistent storage */\n async clear(): Promise<void> {\n await this.ensureInitialized();\n await this.connector.query(`TRUNCATE TABLE ${this.tableName}`);\n }\n\n /**\n * Check if a value exists in the persistent storage\n * @param key - Cache key\n * @returns Promise of true if the value exists, false otherwise\n */\n async has(key: string): Promise<boolean> {\n await this.ensureInitialized();\n const keyHash = this.hashKey(key);\n\n const result = await this.connector.query<{ exists: boolean }>(\n `SELECT EXISTS(SELECT 1 FROM ${this.tableName} WHERE key_hash = $1) as exists`,\n [keyHash],\n );\n\n return result.rows[0]?.exists ?? false;\n }\n\n /**\n * Get the size of the persistent storage\n * @returns Promise of the size of the storage\n */\n async size(): Promise<number> {\n await this.ensureInitialized();\n\n const result = await this.connector.query<{ count: string }>(\n `SELECT COUNT(*) as count FROM ${this.tableName}`,\n );\n return parseInt(result.rows[0]?.count ?? \"0\", 10);\n }\n\n /** Get the total number of bytes in the persistent storage */\n async totalBytes(): Promise<number> {\n await this.ensureInitialized();\n\n const result = await this.connector.query<{ total: string }>(\n `SELECT COALESCE(SUM(byte_size), 0) as total FROM ${this.tableName}`,\n );\n return parseInt(result.rows[0]?.total ?? \"0\", 10);\n }\n\n /**\n * Check if the persistent storage is persistent\n * @returns true if the storage is persistent, false otherwise\n */\n isPersistent(): boolean {\n return true;\n }\n\n /**\n * Check if the persistent storage is healthy\n * @returns Promise of true if the storage is healthy, false otherwise\n */\n async healthCheck(): Promise<boolean> {\n try {\n return await this.connector.healthCheck();\n } catch {\n return false;\n }\n }\n\n /** Close the persistent storage */\n async close(): Promise<void> {\n await this.connector.close();\n }\n\n /**\n * Cleanup expired entries from the persistent storage\n * @returns Promise of the number of expired entries\n */\n async cleanupExpired(): Promise<number> {\n await this.ensureInitialized();\n const result = await this.connector.query<{ count: string }>(\n `WITH deleted as (DELETE FROM ${this.tableName} WHERE expiry < $1 RETURNING *) SELECT COUNT(*) as count FROM deleted`,\n [Date.now()],\n );\n return parseInt(result.rows[0]?.count ?? \"0\", 10);\n }\n\n /** Evict entries from the persistent storage by size */\n private async evictBySize(requiredBytes: number): Promise<void> {\n const freedByExpiry = await this.cleanupExpired();\n if (freedByExpiry > 0) {\n const currentBytes = await this.totalBytes();\n if (currentBytes + requiredBytes <= this.maxBytes) {\n return;\n }\n }\n\n await this.connector.query(\n `DELETE FROM ${this.tableName} WHERE key_hash IN\n (SELECT key_hash FROM ${this.tableName} ORDER BY last_accessed ASC LIMIT $1)`,\n [this.evictionBatchSize],\n );\n }\n\n /** Ensure the persistent storage is initialized */\n private async ensureInitialized(): Promise<void> {\n if (!this.initialized) {\n await this.initialize();\n }\n }\n\n /** Generate a 64-bit hash for the cache key using SHA256 */\n private hashKey(key: string): bigint {\n if (!key) throw ValidationError.missingField(\"key\");\n const hash = createHash(\"sha256\").update(key).digest();\n return hash.readBigInt64BE(0);\n }\n\n /** Serialize a value to a buffer */\n private serializeValue<T>(value: T): Buffer {\n return Buffer.from(JSON.stringify(value), \"utf-8\");\n }\n\n /** Deserialize a value from a buffer */\n private deserializeValue<T>(buffer: Buffer): T {\n return JSON.parse(buffer.toString(\"utf-8\")) as T;\n }\n\n /** Run migrations for the persistent storage */\n private async runMigrations(): Promise<void> {\n try {\n await this.connector.query(`\n CREATE TABLE IF NOT EXISTS ${this.tableName} (\n id BIGSERIAL PRIMARY KEY,\n key_hash BIGINT NOT NULL,\n key BYTEA NOT NULL,\n value BYTEA NOT NULL,\n byte_size INTEGER NOT NULL,\n expiry BIGINT NOT NULL,\n created_at TIMESTAMP NOT NULL DEFAULT NOW(),\n last_accessed TIMESTAMP NOT NULL DEFAULT NOW()\n )\n `);\n\n // unique index on key_hash for fast lookups\n await this.connector.query(\n `CREATE UNIQUE INDEX IF NOT EXISTS idx_${this.tableName}_key_hash ON ${this.tableName} (key_hash);`,\n );\n\n // index on expiry for cleanup queries\n await this.connector.query(\n `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_expiry ON ${this.tableName} (expiry); `,\n );\n\n // index on last_accessed for LRU eviction\n await this.connector.query(\n `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_last_accessed ON ${this.tableName} (last_accessed); `,\n );\n\n // index on byte_size for monitoring\n await this.connector.query(\n `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_byte_size ON ${this.tableName} (byte_size); `,\n );\n } catch (error) {\n logger.error(\n \"Error in running migrations for persistent storage: %O\",\n error,\n );\n throw InitializationError.migrationFailed(error as Error);\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,369 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { Command } from "commander";
5
+ import { Lang, parse } from "@ast-grep/napi";
6
+ import Ajv from "ajv";
7
+ import addFormats from "ajv-formats";
8
+
9
+ //#region src/cli/commands/plugins-sync.ts
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const PLUGIN_MANIFEST_SCHEMA_PATH = path.join(__dirname, "..", "..", "..", "schemas", "plugin-manifest.schema.json");
12
+ /**
13
+ * Checks whether a resolved file path is within a given directory boundary.
14
+ * Uses path.resolve + startsWith to prevent directory traversal.
15
+ *
16
+ * @param filePath - The path to check (will be resolved to absolute)
17
+ * @param boundary - The directory that must contain filePath
18
+ * @returns true if filePath is inside boundary (or equal to it)
19
+ */
20
+ function isWithinDirectory(filePath, boundary) {
21
+ const resolvedPath = path.resolve(filePath);
22
+ const resolvedBoundary = path.resolve(boundary);
23
+ return resolvedPath === resolvedBoundary || resolvedPath.startsWith(`${resolvedBoundary}${path.sep}`);
24
+ }
25
+ let pluginManifestValidator = null;
26
+ /**
27
+ * Loads and compiles the plugin-manifest JSON schema (cached).
28
+ * Returns the compiled validate function or null if the schema cannot be loaded.
29
+ */
30
+ function getPluginManifestValidator() {
31
+ if (pluginManifestValidator) return pluginManifestValidator;
32
+ try {
33
+ const schemaRaw = fs.readFileSync(PLUGIN_MANIFEST_SCHEMA_PATH, "utf-8");
34
+ const schema = JSON.parse(schemaRaw);
35
+ const ajv = new Ajv({
36
+ allErrors: true,
37
+ strict: false
38
+ });
39
+ addFormats(ajv);
40
+ pluginManifestValidator = ajv.compile(schema);
41
+ return pluginManifestValidator;
42
+ } catch (err) {
43
+ console.warn("Warning: Could not load plugin-manifest schema for validation:", err instanceof Error ? err.message : err);
44
+ return null;
45
+ }
46
+ }
47
+ /**
48
+ * Validates a parsed JSON object against the plugin-manifest JSON schema.
49
+ * Returns the manifest if valid, or null and logs schema errors.
50
+ *
51
+ * @param obj - The parsed JSON object to validate
52
+ * @param sourcePath - Path to the manifest file (for warning messages)
53
+ * @returns A valid PluginManifest or null
54
+ */
55
+ function validateManifestWithSchema(obj, sourcePath) {
56
+ if (!obj || typeof obj !== "object") {
57
+ console.warn(`Warning: Manifest at ${sourcePath} is not a valid object`);
58
+ return null;
59
+ }
60
+ const validate = getPluginManifestValidator();
61
+ if (!validate) {
62
+ const m = obj;
63
+ if (typeof m.name === "string" && m.name.length > 0 && typeof m.displayName === "string" && m.displayName.length > 0 && typeof m.description === "string" && m.description.length > 0 && m.resources && typeof m.resources === "object" && Array.isArray(m.resources.required)) return obj;
64
+ console.warn(`Warning: Manifest at ${sourcePath} has invalid structure`);
65
+ return null;
66
+ }
67
+ if (validate(obj)) return obj;
68
+ const message = (validate.errors ?? []).map((e) => ` ${e.instancePath || "/"} ${e.message}${e.params ? ` (${JSON.stringify(e.params)})` : ""}`).join("\n");
69
+ console.warn(`Warning: Manifest at ${sourcePath} failed schema validation:\n${message}`);
70
+ return null;
71
+ }
72
+ /**
73
+ * Known packages that may contain AppKit plugins.
74
+ * Always scanned for manifests, even if not imported in the server file.
75
+ */
76
+ const KNOWN_PLUGIN_PACKAGES = ["@databricks/appkit"];
77
+ /**
78
+ * Candidate paths for the server entry file, relative to cwd.
79
+ * Checked in order; the first that exists is used.
80
+ */
81
+ const SERVER_FILE_CANDIDATES = ["server/server.ts"];
82
+ /**
83
+ * Find the server entry file by checking candidate paths in order.
84
+ *
85
+ * @param cwd - Current working directory
86
+ * @returns Absolute path to the server file, or null if none found
87
+ */
88
+ function findServerFile(cwd) {
89
+ for (const candidate of SERVER_FILE_CANDIDATES) {
90
+ const fullPath = path.join(cwd, candidate);
91
+ if (fs.existsSync(fullPath)) return fullPath;
92
+ }
93
+ return null;
94
+ }
95
+ /**
96
+ * Extract all named imports from the AST root using structural node traversal.
97
+ * Handles single/double quotes, multiline imports, and aliased imports.
98
+ *
99
+ * @param root - AST root node
100
+ * @returns Array of parsed imports with name, original name, and source
101
+ */
102
+ function parseImports(root) {
103
+ const imports = [];
104
+ const importStatements = root.findAll({ rule: { kind: "import_statement" } });
105
+ for (const stmt of importStatements) {
106
+ const sourceNode = stmt.find({ rule: { kind: "string" } });
107
+ if (!sourceNode) continue;
108
+ const source = sourceNode.text().replace(/^['"]|['"]$/g, "");
109
+ const namedImports = stmt.find({ rule: { kind: "named_imports" } });
110
+ if (!namedImports) continue;
111
+ const specifiers = namedImports.findAll({ rule: { kind: "import_specifier" } });
112
+ for (const specifier of specifiers) {
113
+ const children = specifier.children();
114
+ if (children.length >= 3) {
115
+ const originalName = children[0].text();
116
+ const localName = children[children.length - 1].text();
117
+ imports.push({
118
+ name: localName,
119
+ originalName,
120
+ source
121
+ });
122
+ } else {
123
+ const name = specifier.text();
124
+ imports.push({
125
+ name,
126
+ originalName: name,
127
+ source
128
+ });
129
+ }
130
+ }
131
+ }
132
+ return imports;
133
+ }
134
+ /**
135
+ * Extract local names of plugins actually used in the `plugins: [...]` array
136
+ * passed to `createApp()`. Uses structural AST traversal to find `pair` nodes
137
+ * with key "plugins" and array values containing call expressions.
138
+ *
139
+ * @param root - AST root node
140
+ * @returns Set of local variable names used as plugin calls in the plugins array
141
+ */
142
+ function parsePluginUsages(root) {
143
+ const usedNames = /* @__PURE__ */ new Set();
144
+ const pairs = root.findAll({ rule: { kind: "pair" } });
145
+ for (const pair of pairs) {
146
+ const key = pair.find({ rule: { kind: "property_identifier" } });
147
+ if (!key || key.text() !== "plugins") continue;
148
+ const arrayNode = pair.find({ rule: { kind: "array" } });
149
+ if (!arrayNode) continue;
150
+ for (const child of arrayNode.children()) if (child.kind() === "call_expression") {
151
+ const callee = child.children()[0];
152
+ if (callee?.kind() === "identifier") usedNames.add(callee.text());
153
+ }
154
+ }
155
+ return usedNames;
156
+ }
157
+ /**
158
+ * File extensions to try when resolving a relative import to a file path.
159
+ */
160
+ const RESOLVE_EXTENSIONS = [
161
+ ".ts",
162
+ ".tsx",
163
+ ".js",
164
+ ".jsx"
165
+ ];
166
+ /**
167
+ * Resolve a relative import source to the plugin directory containing a manifest.json.
168
+ * Follows the convention that plugins live in their own directory with a manifest.json.
169
+ *
170
+ * Resolution strategy:
171
+ * 1. If the import path is a directory, look for manifest.json directly in it
172
+ * 2. If the import path + extension is a file, look for manifest.json in its parent directory
173
+ * 3. If the import path is a directory with an index file, look for manifest.json in that directory
174
+ *
175
+ * @param importSource - The relative import specifier (e.g. "./plugins/my-plugin")
176
+ * @param serverFileDir - Absolute path to the directory containing the server file
177
+ * @returns Absolute path to manifest.json, or null if not found
178
+ */
179
+ function resolveLocalManifest(importSource, serverFileDir, projectRoot) {
180
+ const resolved = path.resolve(serverFileDir, importSource);
181
+ if (!isWithinDirectory(resolved, projectRoot || serverFileDir)) {
182
+ console.warn(`Warning: Skipping import "${importSource}" — resolves outside the project directory`);
183
+ return null;
184
+ }
185
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
186
+ const manifestPath = path.join(resolved, "manifest.json");
187
+ if (fs.existsSync(manifestPath)) return manifestPath;
188
+ }
189
+ for (const ext of RESOLVE_EXTENSIONS) {
190
+ const filePath = `${resolved}${ext}`;
191
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
192
+ const dir = path.dirname(filePath);
193
+ const manifestPath = path.join(dir, "manifest.json");
194
+ if (fs.existsSync(manifestPath)) return manifestPath;
195
+ break;
196
+ }
197
+ }
198
+ for (const ext of RESOLVE_EXTENSIONS) {
199
+ const indexPath = path.join(resolved, `index${ext}`);
200
+ if (fs.existsSync(indexPath)) {
201
+ const manifestPath = path.join(resolved, "manifest.json");
202
+ if (fs.existsSync(manifestPath)) return manifestPath;
203
+ break;
204
+ }
205
+ }
206
+ return null;
207
+ }
208
+ /**
209
+ * Discover plugin manifests from local (relative) imports in the server file.
210
+ * Resolves each relative import to a directory and looks for manifest.json.
211
+ *
212
+ * @param relativeImports - Parsed imports with relative sources (starting with . or /)
213
+ * @param serverFileDir - Absolute path to the directory containing the server file
214
+ * @param cwd - Current working directory (for computing relative paths in output)
215
+ * @returns Map of plugin name to template plugin entry for local plugins
216
+ */
217
+ function discoverLocalPlugins(relativeImports, serverFileDir, cwd) {
218
+ const plugins = {};
219
+ for (const imp of relativeImports) {
220
+ const manifestPath = resolveLocalManifest(imp.source, serverFileDir, cwd);
221
+ if (!manifestPath) continue;
222
+ try {
223
+ const content = fs.readFileSync(manifestPath, "utf-8");
224
+ const manifest = validateManifestWithSchema(JSON.parse(content), manifestPath);
225
+ if (!manifest) continue;
226
+ const relativePath = path.relative(cwd, path.dirname(manifestPath));
227
+ plugins[manifest.name] = {
228
+ name: manifest.name,
229
+ displayName: manifest.displayName,
230
+ description: manifest.description,
231
+ package: `./${relativePath}`,
232
+ resources: manifest.resources
233
+ };
234
+ } catch (error) {
235
+ console.warn(`Warning: Failed to parse manifest at ${manifestPath}:`, error instanceof Error ? error.message : error);
236
+ }
237
+ }
238
+ return plugins;
239
+ }
240
+ /**
241
+ * Discover plugin manifests from a package's dist folder.
242
+ * Looks for manifest.json files in dist/plugins/{plugin-name}/ directories.
243
+ *
244
+ * @param packagePath - Path to the package in node_modules
245
+ * @returns Array of plugin manifests found in the package
246
+ */
247
+ function discoverPluginManifests(packagePath) {
248
+ const pluginsDir = path.join(packagePath, "dist", "plugins");
249
+ const manifests = [];
250
+ if (!fs.existsSync(pluginsDir)) return manifests;
251
+ const entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
252
+ for (const entry of entries) if (entry.isDirectory()) {
253
+ const manifestPath = path.join(pluginsDir, entry.name, "manifest.json");
254
+ if (fs.existsSync(manifestPath)) try {
255
+ const content = fs.readFileSync(manifestPath, "utf-8");
256
+ const manifest = validateManifestWithSchema(JSON.parse(content), manifestPath);
257
+ if (manifest) manifests.push(manifest);
258
+ } catch (error) {
259
+ console.warn(`Warning: Failed to parse manifest at ${manifestPath}:`, error instanceof Error ? error.message : error);
260
+ }
261
+ }
262
+ return manifests;
263
+ }
264
+ /**
265
+ * Scan node_modules for packages with plugin manifests.
266
+ *
267
+ * @param cwd - Current working directory to search from
268
+ * @param packages - Set of npm package names to scan for plugin manifests
269
+ * @returns Map of plugin name to template plugin entry
270
+ */
271
+ function scanForPlugins(cwd, packages) {
272
+ const plugins = {};
273
+ for (const packageName of packages) {
274
+ const packagePath = path.join(cwd, "node_modules", packageName);
275
+ if (!fs.existsSync(packagePath)) continue;
276
+ const manifests = discoverPluginManifests(packagePath);
277
+ for (const manifest of manifests) plugins[manifest.name] = {
278
+ name: manifest.name,
279
+ displayName: manifest.displayName,
280
+ description: manifest.description,
281
+ package: packageName,
282
+ resources: manifest.resources
283
+ };
284
+ }
285
+ return plugins;
286
+ }
287
+ /**
288
+ * Run the plugins sync command.
289
+ * Parses the server entry file to discover which packages to scan for plugin
290
+ * manifests, then marks plugins that are actually used in the `plugins: [...]`
291
+ * array as requiredByTemplate.
292
+ */
293
+ function runPluginsSync(options) {
294
+ const cwd = process.cwd();
295
+ const outputPath = path.resolve(cwd, options.output || "appkit.plugins.json");
296
+ if (!isWithinDirectory(outputPath, cwd)) {
297
+ console.error(`Error: Output path "${options.output}" resolves outside the project directory.`);
298
+ process.exit(1);
299
+ }
300
+ console.log("Scanning for AppKit plugins...\n");
301
+ const serverFile = findServerFile(cwd);
302
+ let serverImports = [];
303
+ let pluginUsages = /* @__PURE__ */ new Set();
304
+ if (serverFile) {
305
+ const relativePath = path.relative(cwd, serverFile);
306
+ console.log(`Server entry file: ${relativePath}`);
307
+ const content = fs.readFileSync(serverFile, "utf-8");
308
+ const root = parse(serverFile.endsWith(".tsx") ? Lang.Tsx : Lang.TypeScript, content).root();
309
+ serverImports = parseImports(root);
310
+ pluginUsages = parsePluginUsages(root);
311
+ } else console.log("No server entry file found. Checked:", SERVER_FILE_CANDIDATES.join(", "));
312
+ const npmImports = serverImports.filter((i) => !i.source.startsWith(".") && !i.source.startsWith("/"));
313
+ const localImports = serverImports.filter((i) => i.source.startsWith(".") || i.source.startsWith("/"));
314
+ const npmPackages = new Set([...KNOWN_PLUGIN_PACKAGES, ...npmImports.map((i) => i.source)]);
315
+ const plugins = scanForPlugins(cwd, npmPackages);
316
+ if (serverFile && localImports.length > 0) {
317
+ const localPlugins = discoverLocalPlugins(localImports, path.dirname(serverFile), cwd);
318
+ Object.assign(plugins, localPlugins);
319
+ }
320
+ const pluginCount = Object.keys(plugins).length;
321
+ if (pluginCount === 0) {
322
+ console.log("No plugins found.");
323
+ console.log("\nMake sure you have plugin packages installed:");
324
+ for (const pkg of npmPackages) console.log(` - ${pkg}`);
325
+ process.exit(1);
326
+ }
327
+ const serverFileDir = serverFile ? path.dirname(serverFile) : cwd;
328
+ for (const imp of serverImports) {
329
+ if (!pluginUsages.has(imp.name)) continue;
330
+ const isLocal = imp.source.startsWith(".") || imp.source.startsWith("/");
331
+ let plugin;
332
+ if (isLocal) {
333
+ const resolvedImportDir = path.resolve(serverFileDir, imp.source);
334
+ plugin = Object.values(plugins).find((p) => {
335
+ if (!p.package.startsWith(".")) return false;
336
+ return path.resolve(cwd, p.package) === resolvedImportDir && p.name === imp.originalName;
337
+ });
338
+ } else plugin = Object.values(plugins).find((p) => p.package === imp.source && p.name === imp.originalName);
339
+ if (plugin) plugin.requiredByTemplate = true;
340
+ }
341
+ console.log(`\nFound ${pluginCount} plugin(s):`);
342
+ for (const [name, manifest] of Object.entries(plugins)) {
343
+ const resourceCount = manifest.resources.required.length + manifest.resources.optional.length;
344
+ const resourceInfo = resourceCount > 0 ? ` [${resourceCount} resource(s)]` : "";
345
+ const mandatoryTag = manifest.requiredByTemplate ? " (mandatory)" : "";
346
+ console.log(` ${manifest.requiredByTemplate ? "●" : "○"} ${manifest.displayName} (${name}) from ${manifest.package}${resourceInfo}${mandatoryTag}`);
347
+ }
348
+ const templateManifest = {
349
+ $schema: "https://databricks.github.io/appkit/schemas/template-plugins.schema.json",
350
+ version: "1.0",
351
+ plugins
352
+ };
353
+ if (options.write) {
354
+ fs.writeFileSync(outputPath, `${JSON.stringify(templateManifest, null, 2)}\n`);
355
+ console.log(`\n✓ Wrote ${outputPath}`);
356
+ } else {
357
+ console.log("\nTo write the manifest, run:");
358
+ console.log(" npx appkit plugins sync --write\n");
359
+ console.log("Preview:");
360
+ console.log("─".repeat(60));
361
+ console.log(JSON.stringify(templateManifest, null, 2));
362
+ console.log("─".repeat(60));
363
+ }
364
+ }
365
+ const pluginsSyncCommand = new Command("sync").description("Sync plugin manifests from installed packages into appkit.plugins.json").option("-w, --write", "Write the manifest file").option("-o, --output <path>", "Output file path (default: ./appkit.plugins.json)").action(runPluginsSync);
366
+
367
+ //#endregion
368
+ export { pluginsSyncCommand };
369
+ //# sourceMappingURL=plugins-sync.js.map