@databricks/appkit 0.36.0 → 0.38.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 (160) hide show
  1. package/CLAUDE.md +3 -2
  2. package/NOTICE.md +1 -0
  3. package/dist/appkit/package.js +1 -1
  4. package/dist/cache/index.js +1 -2
  5. package/dist/cache/index.js.map +1 -1
  6. package/dist/cache/storage/persistent.js +1 -2
  7. package/dist/cache/storage/persistent.js.map +1 -1
  8. package/dist/cli/commands/plugin/add-resource/add-resource.js.map +1 -1
  9. package/dist/cli/commands/plugin/create/resource-defaults.js +2 -1
  10. package/dist/cli/commands/plugin/create/resource-defaults.js.map +1 -1
  11. package/dist/cli/commands/plugin/schema-resources.js +34 -51
  12. package/dist/cli/commands/plugin/schema-resources.js.map +1 -1
  13. package/dist/cli/commands/plugin/sync/sync.js +20 -6
  14. package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
  15. package/dist/cli/commands/plugin/validate/validate-manifest.js +71 -157
  16. package/dist/cli/commands/plugin/validate/validate-manifest.js.map +1 -1
  17. package/dist/cli/commands/plugin/validate/validate.js +2 -2
  18. package/dist/cli/commands/plugin/validate/validate.js.map +1 -1
  19. package/dist/cli/commands/setup.js +2 -2
  20. package/dist/cli/commands/setup.js.map +1 -1
  21. package/dist/connectors/index.js +0 -1
  22. package/dist/connectors/jobs/client.js +1 -2
  23. package/dist/connectors/jobs/client.js.map +1 -1
  24. package/dist/connectors/lakebase/routing-pool.js +1 -2
  25. package/dist/connectors/lakebase/routing-pool.js.map +1 -1
  26. package/dist/connectors/sql-warehouse/client.js +1 -2
  27. package/dist/connectors/sql-warehouse/client.js.map +1 -1
  28. package/dist/context/execution-context.js +9 -13
  29. package/dist/context/execution-context.js.map +1 -1
  30. package/dist/context/index.js +3 -13
  31. package/dist/context/service-context.js +137 -136
  32. package/dist/context/service-context.js.map +1 -1
  33. package/dist/context/user-context.js +1 -5
  34. package/dist/context/user-context.js.map +1 -1
  35. package/dist/core/agent/create-agent.js +1 -2
  36. package/dist/core/agent/create-agent.js.map +1 -1
  37. package/dist/core/appkit.d.ts.map +1 -1
  38. package/dist/core/appkit.js +9 -37
  39. package/dist/core/appkit.js.map +1 -1
  40. package/dist/errors/authentication.js +44 -40
  41. package/dist/errors/authentication.js.map +1 -1
  42. package/dist/errors/base.js +86 -70
  43. package/dist/errors/base.js.map +1 -1
  44. package/dist/errors/configuration.js +70 -66
  45. package/dist/errors/configuration.js.map +1 -1
  46. package/dist/errors/connection.js +50 -46
  47. package/dist/errors/connection.js.map +1 -1
  48. package/dist/errors/execution.js +47 -43
  49. package/dist/errors/execution.js.map +1 -1
  50. package/dist/errors/index.js +10 -27
  51. package/dist/errors/initialization.js +38 -34
  52. package/dist/errors/initialization.js.map +1 -1
  53. package/dist/errors/server.js +34 -31
  54. package/dist/errors/server.js.map +1 -1
  55. package/dist/errors/tunnel.js +47 -43
  56. package/dist/errors/tunnel.js.map +1 -1
  57. package/dist/errors/validation.js +41 -37
  58. package/dist/errors/validation.js.map +1 -1
  59. package/dist/index.d.ts +1 -1
  60. package/dist/index.js +3 -9
  61. package/dist/plugin/dev-reader.js +1 -2
  62. package/dist/plugin/dev-reader.js.map +1 -1
  63. package/dist/plugin/interceptors/retry.js +1 -2
  64. package/dist/plugin/interceptors/retry.js.map +1 -1
  65. package/dist/plugin/interceptors/telemetry.js +1 -2
  66. package/dist/plugin/interceptors/telemetry.js.map +1 -1
  67. package/dist/plugin/plugin.d.ts +12 -4
  68. package/dist/plugin/plugin.d.ts.map +1 -1
  69. package/dist/plugin/plugin.js +59 -28
  70. package/dist/plugin/plugin.js.map +1 -1
  71. package/dist/plugins/agents/agents.d.ts +56 -3
  72. package/dist/plugins/agents/agents.d.ts.map +1 -1
  73. package/dist/plugins/agents/agents.js +160 -8
  74. package/dist/plugins/agents/agents.js.map +1 -1
  75. package/dist/plugins/agents/schemas.js +0 -1
  76. package/dist/plugins/agents/schemas.js.map +1 -1
  77. package/dist/plugins/analytics/analytics.js +1 -2
  78. package/dist/plugins/analytics/analytics.js.map +1 -1
  79. package/dist/plugins/analytics/manifest.js +9 -1
  80. package/dist/plugins/analytics/query.js +2 -4
  81. package/dist/plugins/analytics/query.js.map +1 -1
  82. package/dist/plugins/files/manifest.js +7 -1
  83. package/dist/plugins/files/plugin.js +3 -6
  84. package/dist/plugins/files/plugin.js.map +1 -1
  85. package/dist/plugins/genie/genie.js +1 -2
  86. package/dist/plugins/genie/genie.js.map +1 -1
  87. package/dist/plugins/genie/manifest.js +7 -1
  88. package/dist/plugins/jobs/plugin.js +2 -4
  89. package/dist/plugins/jobs/plugin.js.map +1 -1
  90. package/dist/plugins/lakebase/lakebase.js +1 -2
  91. package/dist/plugins/lakebase/lakebase.js.map +1 -1
  92. package/dist/plugins/lakebase/manifest.js +26 -4
  93. package/dist/plugins/server/index.js +1 -2
  94. package/dist/plugins/server/index.js.map +1 -1
  95. package/dist/plugins/server/vite-dev-server.js +1 -2
  96. package/dist/plugins/server/vite-dev-server.js.map +1 -1
  97. package/dist/plugins/serving/serving.js +1 -2
  98. package/dist/plugins/serving/serving.js.map +1 -1
  99. package/dist/registry/index.d.ts +1 -1
  100. package/dist/registry/manifest-loader.d.ts +4 -4
  101. package/dist/registry/manifest-loader.d.ts.map +1 -1
  102. package/dist/registry/manifest-loader.js +1 -2
  103. package/dist/registry/manifest-loader.js.map +1 -1
  104. package/dist/registry/resource-registry.js +1 -2
  105. package/dist/registry/resource-registry.js.map +1 -1
  106. package/dist/registry/types.d.ts +21 -7
  107. package/dist/registry/types.d.ts.map +1 -1
  108. package/dist/registry/types.generated.d.ts +1 -1
  109. package/dist/registry/types.generated.js +1 -1
  110. package/dist/registry/types.generated.js.map +1 -1
  111. package/dist/registry/types.js +3 -3
  112. package/dist/registry/types.js.map +1 -1
  113. package/dist/schemas/manifest.d.ts +1139 -0
  114. package/dist/schemas/manifest.d.ts.map +1 -0
  115. package/dist/schemas/manifest.js +524 -0
  116. package/dist/schemas/manifest.js.map +1 -0
  117. package/dist/shared/src/index.d.ts +1 -1
  118. package/dist/shared/src/plugin.d.ts +38 -7
  119. package/dist/shared/src/plugin.d.ts.map +1 -1
  120. package/dist/shared/src/schemas/manifest.d.ts +1110 -0
  121. package/dist/shared/src/schemas/manifest.d.ts.map +1 -0
  122. package/dist/stream/arrow-stream-processor.js +1 -2
  123. package/dist/stream/arrow-stream-processor.js.map +1 -1
  124. package/dist/stream/buffers.js +1 -2
  125. package/dist/stream/buffers.js.map +1 -1
  126. package/docs/api/appkit/Enumeration.ResourceType.md +1 -1
  127. package/docs/api/appkit/Interface.PluginManifest.md +57 -3
  128. package/docs/api/appkit/Interface.ResourceEntry.md +6 -4
  129. package/docs/api/appkit/Interface.ResourceRequirement.md +7 -61
  130. package/docs/api/appkit/TypeAlias.ResourceFieldEntry.md +6 -0
  131. package/docs/api/appkit/Variable.agents.md +1 -1
  132. package/docs/api/appkit.md +12 -12
  133. package/docs/app-management.md +1 -1
  134. package/docs/development/ai-assisted-development.md +2 -2
  135. package/docs/development/local-development.md +1 -1
  136. package/docs/development/remote-bridge.md +1 -1
  137. package/docs/development/templates.md +118 -12
  138. package/docs/development.md +1 -1
  139. package/docs/plugins/agents.md +8 -4
  140. package/docs/plugins/custom-plugins.md +33 -23
  141. package/docs/plugins/lakebase.md +1 -1
  142. package/docs/plugins/manifest.md +293 -0
  143. package/docs.md +2 -2
  144. package/llms.txt +3 -2
  145. package/package.json +3 -3
  146. package/sbom.cdx.json +1 -1
  147. package/dist/_virtual/_rolldown/runtime.js +0 -7
  148. package/dist/connectors/lakebase-v1/client.js +0 -15
  149. package/dist/connectors/lakebase-v1/client.js.map +0 -1
  150. package/dist/connectors/lakebase-v1/index.js +0 -3
  151. package/dist/context/index.js.map +0 -1
  152. package/dist/errors/index.js.map +0 -1
  153. package/dist/index.js.map +0 -1
  154. package/dist/schemas/plugin-manifest.generated.d.ts +0 -182
  155. package/dist/schemas/plugin-manifest.generated.d.ts.map +0 -1
  156. package/dist/schemas/plugin-manifest.schema.json +0 -489
  157. package/dist/schemas/template-plugins.schema.json +0 -113
  158. package/dist/shared/src/schemas/plugin-manifest.generated.d.ts +0 -182
  159. package/dist/shared/src/schemas/plugin-manifest.generated.d.ts.map +0 -1
  160. package/docs/api/appkit/Interface.ResourceFieldEntry.md +0 -82
package/CLAUDE.md CHANGED
@@ -52,6 +52,7 @@ npx @databricks/appkit docs <query>
52
52
  - [Genie plugin](./docs/plugins/genie.md): Integrates Databricks AI/BI Genie spaces into your AppKit application, enabling natural language data queries via a conversational interface.
53
53
  - [Jobs plugin](./docs/plugins/jobs.md): Trigger and monitor Databricks Lakeflow Jobs from your AppKit application.
54
54
  - [Lakebase plugin](./docs/plugins/lakebase.md): Provides a PostgreSQL connection pool for Databricks Lakebase Autoscaling with automatic OAuth token refresh.
55
+ - [Plugin manifest](./docs/plugins/manifest.md): Every plugin ships a manifest.json next to its source code. The manifest declares plugin metadata, the Databricks resources the plugin needs, and any structured rules a scaffolding agent must honor when running databricks apps init. It is consumed at three stages:
55
56
  - [Model Serving plugin](./docs/plugins/model-serving.md): Provides an authenticated proxy to Databricks Model Serving endpoints, with invoke and streaming support.
56
57
  - [Plugin management](./docs/plugins/plugin-management.md): AppKit includes a CLI for managing plugins. All commands are available under npx @databricks/appkit plugin.
57
58
  - [Server plugin](./docs/plugins/server.md): Provides HTTP server capabilities with development and production modes.
@@ -76,7 +77,7 @@ npx @databricks/appkit docs <query>
76
77
  - [Class: TunnelError](./docs/api/appkit/Class.TunnelError.md): Error thrown when remote tunnel operations fail.
77
78
  - [Class: ValidationError](./docs/api/appkit/Class.ValidationError.md): Error thrown when input validation fails.
78
79
  - [Enumeration: RequestedClaimsPermissionSet](./docs/api/appkit/Enumeration.RequestedClaimsPermissionSet.md): Permission set for Unity Catalog table access
79
- - [Enumeration: ResourceType](./docs/api/appkit/Enumeration.ResourceType.md): Resource types from schema $defs.resourceType.enum
80
+ - [Enumeration: ResourceType](./docs/api/appkit/Enumeration.ResourceType.md): Resource types from resourceTypeSchema.options
80
81
  - [Function: agentIdFromMarkdownPath()](./docs/api/appkit/Function.agentIdFromMarkdownPath.md): Derives the logical agent id from a markdown path. When the file is named
81
82
  - [Function: appKitServingTypesPlugin()](./docs/api/appkit/Function.appKitServingTypesPlugin.md): Vite plugin to generate TypeScript types for AppKit serving endpoints.
82
83
  - [Function: appKitTypesPlugin()](./docs/api/appkit/Function.appKitTypesPlugin.md): Vite plugin to generate types for AppKit queries.
@@ -141,7 +142,6 @@ npx @databricks/appkit docs <query>
141
142
  - [Interface: RequestedClaims](./docs/api/appkit/Interface.RequestedClaims.md): Optional claims for fine-grained Unity Catalog table permissions
142
143
  - [Interface: RequestedResource](./docs/api/appkit/Interface.RequestedResource.md): Resource to request permissions for in Unity Catalog
143
144
  - [Interface: ResourceEntry](./docs/api/appkit/Interface.ResourceEntry.md): Internal representation of a resource in the registry.
144
- - [Interface: ResourceFieldEntry](./docs/api/appkit/Interface.ResourceFieldEntry.md): Defines a single field for a resource. Each field has its own environment variable and optional description. Single-value types use one key (e.g. id); multi-value types (database, secret) use multiple (e.g. instancename, databasename or scope, key).
145
145
  - [Interface: ResourceRequirement](./docs/api/appkit/Interface.ResourceRequirement.md): Declares a resource requirement for a plugin.
146
146
  - [Interface: RunAgentInput](./docs/api/appkit/Interface.RunAgentInput.md): Properties
147
147
  - [Interface: RunAgentResult](./docs/api/appkit/Interface.RunAgentResult.md): Properties
@@ -174,6 +174,7 @@ npx @databricks/appkit docs <query>
174
174
  - [Type Alias: PluginData<T, U, N>](./docs/api/appkit/TypeAlias.PluginData.md): Tuple of plugin class, config, and name. Created by toPlugin() and passed to createApp().
175
175
  - [Type Alias: Plugins](./docs/api/appkit/TypeAlias.Plugins.md): Plugin map passed to the function form of AgentDefinition.tools.
176
176
  - [Type Alias: ResolvedToolEntry](./docs/api/appkit/TypeAlias.ResolvedToolEntry.md): Internal tool-index entry after a tool record has been resolved to a dispatchable form.
177
+ - [Type Alias: ResourceFieldEntry](./docs/api/appkit/TypeAlias.ResourceFieldEntry.md)
177
178
  - [Type Alias: ResourcePermission](./docs/api/appkit/TypeAlias.ResourcePermission.md): Union of all possible permission levels across all resource types.
178
179
  - [Type Alias: ServingFactory](./docs/api/appkit/TypeAlias.ServingFactory.md): Factory function returned by AppKit.serving.
179
180
  - [Type Alias: ToolRegistry](./docs/api/appkit/TypeAlias.ToolRegistry.md)
package/NOTICE.md CHANGED
@@ -68,6 +68,7 @@ This Software contains code from the following open source projects:
68
68
  | [input-otp](https://www.npmjs.com/package/input-otp) | 1.4.2 | MIT | https://input-otp.rodz.dev/ |
69
69
  | [js-yaml](https://www.npmjs.com/package/js-yaml) | 3.14.2, 4.1.1 | MIT | https://github.com/nodeca/js-yaml#readme |
70
70
  | [lucide-react](https://www.npmjs.com/package/lucide-react) | 0.554.0 | ISC | https://lucide.dev |
71
+ | [magic-string](https://www.npmjs.com/package/magic-string) | 0.30.21 | MIT | https://github.com/Rich-Harris/magic-string#readme |
71
72
  | [marked](https://www.npmjs.com/package/marked) | 16.4.2, 17.0.3 | MIT | https://marked.js.org |
72
73
  | [next-themes](https://www.npmjs.com/package/next-themes) | 0.4.6 | MIT | https://github.com/pacocoursey/next-themes#readme |
73
74
  | [obug](https://www.npmjs.com/package/obug) | 2.1.1 | MIT | https://github.com/sxzz/obug#readme |
@@ -1,6 +1,6 @@
1
1
  //#region package.json
2
2
  var name = "@databricks/appkit";
3
- var version = "0.36.0";
3
+ var version = "0.38.0";
4
4
 
5
5
  //#endregion
6
6
  export { name, version };
@@ -2,7 +2,7 @@ import { createLogger } from "../logging/logger.js";
2
2
  import { AppKitError } from "../errors/base.js";
3
3
  import { ExecutionError } from "../errors/execution.js";
4
4
  import { InitializationError } from "../errors/initialization.js";
5
- import { init_errors } from "../errors/index.js";
5
+ import "../errors/index.js";
6
6
  import { createLakebasePool } from "../connectors/lakebase/index.js";
7
7
  import { TelemetryManager } from "../telemetry/telemetry-manager.js";
8
8
  import { SpanStatusCode } from "../telemetry/index.js";
@@ -15,7 +15,6 @@ import { createHash } from "node:crypto";
15
15
  import { ApiError, WorkspaceClient } from "@databricks/sdk-experimental";
16
16
 
17
17
  //#region src/cache/index.ts
18
- init_errors();
19
18
  const logger = createLogger("cache");
20
19
  /**
21
20
  * Cache manager class to handle cache operations.
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/cache/index.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { ApiError, WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\nimport { createLakebasePool } from \"../connectors/lakebase\";\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 pool = createLakebasePool({ workspaceClient });\n const persistentStorage = new PersistentStorage(config, pool);\n\n const isHealthy = await persistentStorage.healthCheck();\n if (isHealthy) {\n await persistentStorage.initialize();\n return new CacheManager(persistentStorage, config);\n }\n\n // Health check failed, close the pool and fallback\n await pool.end();\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 const cached = await this.getValid<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;\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 // Preserve AppKit errors and Databricks API errors (with status codes)\n // so route handlers can map them to proper HTTP responses.\n if (error instanceof AppKitError || error instanceof ApiError) {\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.getValid<T>(key);\n return entry?.value ?? null;\n }\n\n /**\n * Get a cached entry only if it has not expired.\n * Returns null on miss or expired (and deletes the expired entry).\n *\n * Storage implementations return entries unconditionally — expiry handling\n * lives at the CacheManager layer.\n */\n private async getValid<T>(key: string): Promise<CacheEntry<T> | null> {\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;\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.getValid(key);\n return entry !== null;\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 { ApiError, WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\nimport { createLakebasePool } from \"../connectors/lakebase\";\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 pool = createLakebasePool({ workspaceClient });\n const persistentStorage = new PersistentStorage(config, pool);\n\n const isHealthy = await persistentStorage.healthCheck();\n if (isHealthy) {\n await persistentStorage.initialize();\n return new CacheManager(persistentStorage, config);\n }\n\n // Health check failed, close the pool and fallback\n await pool.end();\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 const cached = await this.getValid<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;\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 // Preserve AppKit errors and Databricks API errors (with status codes)\n // so route handlers can map them to proper HTTP responses.\n if (error instanceof AppKitError || error instanceof ApiError) {\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.getValid<T>(key);\n return entry?.value ?? null;\n }\n\n /**\n * Get a cached entry only if it has not expired.\n * Returns null on miss or expired (and deletes the expired entry).\n *\n * Storage implementations return entries unconditionally — expiry handling\n * lives at the CacheManager layer.\n */\n private async getValid<T>(key: string): Promise<CacheEntry<T> | null> {\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;\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.getValid(key);\n return entry !== null;\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,12 +1,11 @@
1
1
  import { createLogger } from "../../logging/logger.js";
2
2
  import { InitializationError } from "../../errors/initialization.js";
3
3
  import { ValidationError } from "../../errors/validation.js";
4
- import { init_errors } from "../../errors/index.js";
4
+ import "../../errors/index.js";
5
5
  import { lakebaseStorageDefaults } from "./defaults.js";
6
6
  import { createHash } from "node:crypto";
7
7
 
8
8
  //#region src/cache/storage/persistent.ts
9
- init_errors();
10
9
  const logger = createLogger("cache:persistent");
11
10
  /**
12
11
  * Persistent cache storage implementation. Uses a least recently used (LRU) eviction policy
@@ -1 +1 @@
1
- {"version":3,"file":"persistent.js","names":[],"sources":["../../../src/cache/storage/persistent.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type pg from \"pg\";\nimport type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\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 pool = createLakebasePool({ workspaceClient });\n * const persistentStorage = new PersistentStorage(config, pool);\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 pool: pg.Pool;\n private readonly schemaName: string;\n private readonly tableName: string;\n private readonly qualifiedTableName: 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, pool: pg.Pool) {\n this.pool = pool;\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.schemaName = lakebaseStorageDefaults.schemaName;\n this.tableName = lakebaseStorageDefaults.tableName;\n this.qualifiedTableName = `${this.schemaName}.${this.tableName}`;\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.pool.query<{\n value: Buffer;\n expiry: string;\n }>(\n `SELECT value, expiry FROM ${this.qualifiedTableName} 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.pool\n .query(\n `UPDATE ${this.qualifiedTableName} 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.pool.query(\n `INSERT INTO ${this.qualifiedTableName} (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.pool.query(\n `DELETE FROM ${this.qualifiedTableName} 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.pool.query(`TRUNCATE TABLE ${this.qualifiedTableName}`);\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.pool.query<{ exists: boolean }>(\n `SELECT EXISTS(SELECT 1 FROM ${this.qualifiedTableName} 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.pool.query<{ count: string }>(\n `SELECT COUNT(*) as count FROM ${this.qualifiedTableName}`,\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.pool.query<{ total: string }>(\n `SELECT COALESCE(SUM(byte_size), 0) as total FROM ${this.qualifiedTableName}`,\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 await this.pool.query(\"SELECT 1\");\n return true;\n } catch {\n return false;\n }\n }\n\n /** Close the persistent storage */\n async close(): Promise<void> {\n await this.pool.end();\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.pool.query<{ count: string }>(\n `WITH deleted as (DELETE FROM ${this.qualifiedTableName} 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.pool.query(\n `DELETE FROM ${this.qualifiedTableName} WHERE key_hash IN\n (SELECT key_hash FROM ${this.qualifiedTableName} 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 const steps = [\n {\n name: \"create schema\",\n query: `CREATE SCHEMA IF NOT EXISTS ${this.schemaName}`,\n },\n {\n name: \"create table\",\n query: `CREATE TABLE IF NOT EXISTS ${this.qualifiedTableName} (\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 name: \"create index (key_hash)\",\n query: `CREATE UNIQUE INDEX IF NOT EXISTS idx_${this.tableName}_key_hash ON ${this.qualifiedTableName} (key_hash)`,\n },\n {\n name: \"create index (expiry)\",\n query: `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_expiry ON ${this.qualifiedTableName} (expiry)`,\n },\n {\n name: \"create index (last_accessed)\",\n query: `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_last_accessed ON ${this.qualifiedTableName} (last_accessed)`,\n },\n {\n name: \"create index (byte_size)\",\n query: `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_byte_size ON ${this.qualifiedTableName} (byte_size)`,\n },\n ];\n\n for (const step of steps) {\n try {\n await this.pool.query(step.query);\n } catch (error) {\n logger.error(\"Migration step '%s' failed: %O\", step.name, error);\n throw InitializationError.migrationFailed(error as Error);\n }\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 pg from \"pg\";\nimport type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\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 pool = createLakebasePool({ workspaceClient });\n * const persistentStorage = new PersistentStorage(config, pool);\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 pool: pg.Pool;\n private readonly schemaName: string;\n private readonly tableName: string;\n private readonly qualifiedTableName: 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, pool: pg.Pool) {\n this.pool = pool;\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.schemaName = lakebaseStorageDefaults.schemaName;\n this.tableName = lakebaseStorageDefaults.tableName;\n this.qualifiedTableName = `${this.schemaName}.${this.tableName}`;\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.pool.query<{\n value: Buffer;\n expiry: string;\n }>(\n `SELECT value, expiry FROM ${this.qualifiedTableName} 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.pool\n .query(\n `UPDATE ${this.qualifiedTableName} 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.pool.query(\n `INSERT INTO ${this.qualifiedTableName} (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.pool.query(\n `DELETE FROM ${this.qualifiedTableName} 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.pool.query(`TRUNCATE TABLE ${this.qualifiedTableName}`);\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.pool.query<{ exists: boolean }>(\n `SELECT EXISTS(SELECT 1 FROM ${this.qualifiedTableName} 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.pool.query<{ count: string }>(\n `SELECT COUNT(*) as count FROM ${this.qualifiedTableName}`,\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.pool.query<{ total: string }>(\n `SELECT COALESCE(SUM(byte_size), 0) as total FROM ${this.qualifiedTableName}`,\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 await this.pool.query(\"SELECT 1\");\n return true;\n } catch {\n return false;\n }\n }\n\n /** Close the persistent storage */\n async close(): Promise<void> {\n await this.pool.end();\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.pool.query<{ count: string }>(\n `WITH deleted as (DELETE FROM ${this.qualifiedTableName} 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.pool.query(\n `DELETE FROM ${this.qualifiedTableName} WHERE key_hash IN\n (SELECT key_hash FROM ${this.qualifiedTableName} 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 const steps = [\n {\n name: \"create schema\",\n query: `CREATE SCHEMA IF NOT EXISTS ${this.schemaName}`,\n },\n {\n name: \"create table\",\n query: `CREATE TABLE IF NOT EXISTS ${this.qualifiedTableName} (\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 name: \"create index (key_hash)\",\n query: `CREATE UNIQUE INDEX IF NOT EXISTS idx_${this.tableName}_key_hash ON ${this.qualifiedTableName} (key_hash)`,\n },\n {\n name: \"create index (expiry)\",\n query: `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_expiry ON ${this.qualifiedTableName} (expiry)`,\n },\n {\n name: \"create index (last_accessed)\",\n query: `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_last_accessed ON ${this.qualifiedTableName} (last_accessed)`,\n },\n {\n name: \"create index (byte_size)\",\n query: `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_byte_size ON ${this.qualifiedTableName} (byte_size)`,\n },\n ];\n\n for (const step of steps) {\n try {\n await this.pool.query(step.query);\n } catch (error) {\n logger.error(\"Migration step '%s' failed: %O\", step.name, error);\n throw InitializationError.migrationFailed(error as Error);\n }\n }\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"add-resource.js","names":[],"sources":["../../../../../src/cli/commands/plugin/add-resource/add-resource.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport { cancel, intro, outro } from \"@clack/prompts\";\nimport { Command } from \"commander\";\nimport { promptOneResource } from \"../create/prompt-resource\";\nimport {\n DEFAULT_PERMISSION_BY_TYPE,\n getDefaultFieldsForType,\n getValidResourceTypes,\n humanizeResourceType,\n resourceKeyFromType,\n} from \"../create/resource-defaults\";\nimport { resolveManifestInDir } from \"../manifest-resolve\";\nimport type { PluginManifest, ResourceRequirement } from \"../manifest-types\";\nimport { validateManifest } from \"../validate/validate-manifest\";\n\n/** Extended manifest type that preserves extra JSON fields (e.g. $schema, author, version) for round-trip writes. */\ninterface ManifestWithExtras extends PluginManifest {\n [key: string]: unknown;\n}\n\ninterface AddResourceOptions {\n path?: string;\n type?: string;\n required?: boolean;\n resourceKey?: string;\n description?: string;\n permission?: string;\n fieldsJson?: string;\n dryRun?: boolean;\n}\n\nfunction loadManifest(\n pluginDir: string,\n): { manifest: ManifestWithExtras; manifestPath: string } | null {\n const resolved = resolveManifestInDir(pluginDir, { allowJsManifest: true });\n\n if (!resolved) {\n console.error(\n `No manifest found in ${pluginDir}. This command requires manifest.json (manifest.js cannot be edited in place).`,\n );\n console.error(\n \" appkit plugin add-resource --path <dir-with-manifest.json>\",\n );\n process.exit(1);\n }\n\n if (resolved.type !== \"json\") {\n console.error(\n `Editable manifest not found. add-resource only supports plugin directories that contain manifest.json (found ${path.basename(resolved.path)}).`,\n );\n process.exit(1);\n }\n\n const manifestPath = resolved.path;\n\n try {\n const raw = fs.readFileSync(manifestPath, \"utf-8\");\n const parsed = JSON.parse(raw) as unknown;\n const result = validateManifest(parsed);\n if (!result.valid || !result.manifest) {\n console.error(\n \"Invalid manifest. Run `appkit plugin validate` for details.\",\n );\n process.exit(1);\n }\n return { manifest: parsed as ManifestWithExtras, manifestPath };\n } catch (err) {\n console.error(\n \"Failed to read or parse manifest.json:\",\n err instanceof Error ? err.message : err,\n );\n process.exit(1);\n }\n}\n\nfunction buildEntry(\n type: string,\n opts: AddResourceOptions,\n): { entry: ResourceRequirement; isRequired: boolean } {\n const alias = humanizeResourceType(type);\n const isRequired = opts.required !== false;\n\n let fields = getDefaultFieldsForType(type);\n if (opts.fieldsJson) {\n try {\n const parsed = JSON.parse(opts.fieldsJson) as Record<\n string,\n { env: string; description?: string }\n >;\n fields = { ...fields, ...parsed };\n } catch {\n console.error(\"Error: --fields-json must be valid JSON.\");\n console.error(\n ' Example: --fields-json \\'{\"id\":{\"env\":\"MY_WAREHOUSE_ID\"}}\\'',\n );\n process.exit(1);\n }\n }\n\n const entry: ResourceRequirement = {\n type: type as ResourceRequirement[\"type\"],\n alias,\n resourceKey: opts.resourceKey ?? resourceKeyFromType(type),\n description:\n opts.description ||\n `${isRequired ? \"Required\" : \"Optional\"} for ${alias} functionality.`,\n permission:\n opts.permission ?? DEFAULT_PERMISSION_BY_TYPE[type] ?? \"CAN_VIEW\",\n fields,\n };\n\n return { entry, isRequired };\n}\n\nfunction runNonInteractive(opts: AddResourceOptions): void {\n const cwd = process.cwd();\n const pluginDir = path.resolve(cwd, opts.path ?? \".\");\n const loaded = loadManifest(pluginDir);\n if (!loaded) return;\n const { manifest, manifestPath } = loaded;\n\n const type = opts.type as string;\n const validTypes = getValidResourceTypes();\n if (!validTypes.includes(type)) {\n console.error(`Error: Unknown resource type \"${type}\".`);\n console.error(` Valid types: ${validTypes.join(\", \")}`);\n process.exit(1);\n }\n const { entry, isRequired } = buildEntry(type, opts);\n\n if (isRequired) {\n manifest.resources.required.push(entry);\n } else {\n manifest.resources.optional.push(entry);\n }\n\n if (opts.dryRun) {\n console.log(JSON.stringify(manifest, null, 2));\n return;\n }\n\n fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\\n`);\n console.log(\n `Added ${entry.alias} as ${isRequired ? \"required\" : \"optional\"} to ${path.relative(cwd, manifestPath)}`,\n );\n}\n\nasync function runInteractive(opts: AddResourceOptions): Promise<void> {\n intro(\"Add resource to plugin manifest\");\n\n const cwd = process.cwd();\n const pluginDir = path.resolve(cwd, opts.path ?? \".\");\n const loaded = loadManifest(pluginDir);\n if (!loaded) return;\n const { manifest, manifestPath } = loaded;\n\n const spec = await promptOneResource();\n if (!spec) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const alias = humanizeResourceType(spec.type);\n const entry: ResourceRequirement = {\n type: spec.type as ResourceRequirement[\"type\"],\n alias,\n resourceKey: spec.resourceKey,\n description: spec.description || `Required for ${alias} functionality.`,\n permission: spec.permission,\n fields: spec.fields,\n };\n\n if (spec.required) {\n manifest.resources.required.push(entry);\n } else {\n manifest.resources.optional.push(entry);\n }\n\n fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\\n`);\n\n outro(\"Resource added.\");\n console.log(\n `\\nAdded ${alias} as ${spec.required ? \"required\" : \"optional\"} to ${path.relative(cwd, manifestPath)}`,\n );\n}\n\nasync function runPluginAddResource(opts: AddResourceOptions): Promise<void> {\n if (opts.type) {\n runNonInteractive(opts);\n } else {\n await runInteractive(opts);\n }\n}\n\nexport const pluginAddResourceCommand = new Command(\"add-resource\")\n .description(\n \"Add a resource requirement to an existing plugin manifest. Overwrites manifest.json in place.\",\n )\n .option(\n \"-p, --path <dir>\",\n \"Plugin directory containing manifest.json (default: .)\",\n )\n .option(\n \"-t, --type <resource_type>\",\n \"Resource type (e.g. sql_warehouse, volume). Enables non-interactive mode.\",\n )\n .option(\"--required\", \"Mark resource as required (default: true)\", true)\n .option(\"--no-required\", \"Mark resource as optional\")\n .option(\"--resource-key <key>\", \"Resource key (default: derived from type)\")\n .option(\"--description <text>\", \"Description of the resource requirement\")\n .option(\"--permission <perm>\", \"Permission level (default: from schema)\")\n .option(\n \"--fields-json <json>\",\n 'JSON object overriding field env vars (e.g. \\'{\"id\":{\"env\":\"MY_WAREHOUSE_ID\"}}\\')',\n )\n .option(\"--dry-run\", \"Preview the updated manifest without writing\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit plugin add-resource\n $ appkit plugin add-resource --path plugins/my-plugin --type sql_warehouse\n $ appkit plugin add-resource --path plugins/my-plugin --type volume --no-required --dry-run\n $ appkit plugin add-resource --type sql_warehouse --fields-json '{\"id\":{\"env\":\"MY_WAREHOUSE_ID\"}}'`,\n )\n .action((opts) =>\n runPluginAddResource(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":";;;;;;;;;;;AAiCA,SAAS,aACP,WAC+D;CAC/D,MAAM,WAAW,qBAAqB,WAAW,EAAE,iBAAiB,MAAM,CAAC;AAE3E,KAAI,CAAC,UAAU;AACb,UAAQ,MACN,wBAAwB,UAAU,gFACnC;AACD,UAAQ,MACN,+DACD;AACD,UAAQ,KAAK,EAAE;;AAGjB,KAAI,SAAS,SAAS,QAAQ;AAC5B,UAAQ,MACN,gHAAgH,KAAK,SAAS,SAAS,KAAK,CAAC,IAC9I;AACD,UAAQ,KAAK,EAAE;;CAGjB,MAAM,eAAe,SAAS;AAE9B,KAAI;EACF,MAAM,MAAM,GAAG,aAAa,cAAc,QAAQ;EAClD,MAAM,SAAS,KAAK,MAAM,IAAI;EAC9B,MAAM,SAAS,iBAAiB,OAAO;AACvC,MAAI,CAAC,OAAO,SAAS,CAAC,OAAO,UAAU;AACrC,WAAQ,MACN,8DACD;AACD,WAAQ,KAAK,EAAE;;AAEjB,SAAO;GAAE,UAAU;GAA8B;GAAc;UACxD,KAAK;AACZ,UAAQ,MACN,0CACA,eAAe,QAAQ,IAAI,UAAU,IACtC;AACD,UAAQ,KAAK,EAAE;;;AAInB,SAAS,WACP,MACA,MACqD;CACrD,MAAM,QAAQ,qBAAqB,KAAK;CACxC,MAAM,aAAa,KAAK,aAAa;CAErC,IAAI,SAAS,wBAAwB,KAAK;AAC1C,KAAI,KAAK,WACP,KAAI;EACF,MAAM,SAAS,KAAK,MAAM,KAAK,WAAW;AAI1C,WAAS;GAAE,GAAG;GAAQ,GAAG;GAAQ;SAC3B;AACN,UAAQ,MAAM,2CAA2C;AACzD,UAAQ,MACN,oEACD;AACD,UAAQ,KAAK,EAAE;;AAgBnB,QAAO;EAAE,OAZ0B;GAC3B;GACN;GACA,aAAa,KAAK,eAAe,oBAAoB,KAAK;GAC1D,aACE,KAAK,eACL,GAAG,aAAa,aAAa,WAAW,OAAO,MAAM;GACvD,YACE,KAAK,cAAc,2BAA2B,SAAS;GACzD;GACD;EAEe;EAAY;;AAG9B,SAAS,kBAAkB,MAAgC;CACzD,MAAM,MAAM,QAAQ,KAAK;CAEzB,MAAM,SAAS,aADG,KAAK,QAAQ,KAAK,KAAK,QAAQ,IAAI,CACf;AACtC,KAAI,CAAC,OAAQ;CACb,MAAM,EAAE,UAAU,iBAAiB;CAEnC,MAAM,OAAO,KAAK;CAClB,MAAM,aAAa,uBAAuB;AAC1C,KAAI,CAAC,WAAW,SAAS,KAAK,EAAE;AAC9B,UAAQ,MAAM,iCAAiC,KAAK,IAAI;AACxD,UAAQ,MAAM,kBAAkB,WAAW,KAAK,KAAK,GAAG;AACxD,UAAQ,KAAK,EAAE;;CAEjB,MAAM,EAAE,OAAO,eAAe,WAAW,MAAM,KAAK;AAEpD,KAAI,WACF,UAAS,UAAU,SAAS,KAAK,MAAM;KAEvC,UAAS,UAAU,SAAS,KAAK,MAAM;AAGzC,KAAI,KAAK,QAAQ;AACf,UAAQ,IAAI,KAAK,UAAU,UAAU,MAAM,EAAE,CAAC;AAC9C;;AAGF,IAAG,cAAc,cAAc,GAAG,KAAK,UAAU,UAAU,MAAM,EAAE,CAAC,IAAI;AACxE,SAAQ,IACN,SAAS,MAAM,MAAM,MAAM,aAAa,aAAa,WAAW,MAAM,KAAK,SAAS,KAAK,aAAa,GACvG;;AAGH,eAAe,eAAe,MAAyC;AACrE,OAAM,kCAAkC;CAExC,MAAM,MAAM,QAAQ,KAAK;CAEzB,MAAM,SAAS,aADG,KAAK,QAAQ,KAAK,KAAK,QAAQ,IAAI,CACf;AACtC,KAAI,CAAC,OAAQ;CACb,MAAM,EAAE,UAAU,iBAAiB;CAEnC,MAAM,OAAO,MAAM,mBAAmB;AACtC,KAAI,CAAC,MAAM;AACT,SAAO,aAAa;AACpB,UAAQ,KAAK,EAAE;;CAGjB,MAAM,QAAQ,qBAAqB,KAAK,KAAK;CAC7C,MAAM,QAA6B;EACjC,MAAM,KAAK;EACX;EACA,aAAa,KAAK;EAClB,aAAa,KAAK,eAAe,gBAAgB,MAAM;EACvD,YAAY,KAAK;EACjB,QAAQ,KAAK;EACd;AAED,KAAI,KAAK,SACP,UAAS,UAAU,SAAS,KAAK,MAAM;KAEvC,UAAS,UAAU,SAAS,KAAK,MAAM;AAGzC,IAAG,cAAc,cAAc,GAAG,KAAK,UAAU,UAAU,MAAM,EAAE,CAAC,IAAI;AAExE,OAAM,kBAAkB;AACxB,SAAQ,IACN,WAAW,MAAM,MAAM,KAAK,WAAW,aAAa,WAAW,MAAM,KAAK,SAAS,KAAK,aAAa,GACtG;;AAGH,eAAe,qBAAqB,MAAyC;AAC3E,KAAI,KAAK,KACP,mBAAkB,KAAK;KAEvB,OAAM,eAAe,KAAK;;AAI9B,MAAa,2BAA2B,IAAI,QAAQ,eAAe,CAChE,YACC,gGACD,CACA,OACC,oBACA,yDACD,CACA,OACC,8BACA,4EACD,CACA,OAAO,cAAc,6CAA6C,KAAK,CACvE,OAAO,iBAAiB,4BAA4B,CACpD,OAAO,wBAAwB,4CAA4C,CAC3E,OAAO,wBAAwB,0CAA0C,CACzE,OAAO,uBAAuB,0CAA0C,CACxE,OACC,wBACA,wFACD,CACA,OAAO,aAAa,+CAA+C,CACnE,YACC,SACA;;;;;sGAMD,CACA,QAAQ,SACP,qBAAqB,KAAK,CAAC,OAAO,QAAQ;AACxC,SAAQ,MAAM,IAAI;AAClB,SAAQ,KAAK,EAAE;EACf,CACH"}
1
+ {"version":3,"file":"add-resource.js","names":[],"sources":["../../../../../src/cli/commands/plugin/add-resource/add-resource.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport { cancel, intro, outro } from \"@clack/prompts\";\nimport { Command } from \"commander\";\nimport { promptOneResource } from \"../create/prompt-resource\";\nimport {\n DEFAULT_PERMISSION_BY_TYPE,\n getDefaultFieldsForType,\n getValidResourceTypes,\n humanizeResourceType,\n resourceKeyFromType,\n} from \"../create/resource-defaults\";\nimport { resolveManifestInDir } from \"../manifest-resolve\";\nimport type { PluginManifest, ResourceRequirement } from \"../manifest-types\";\nimport { validateManifest } from \"../validate/validate-manifest\";\n\n/** Extended manifest type that preserves extra JSON fields (e.g. $schema, author, version) for round-trip writes. */\ninterface ManifestWithExtras extends PluginManifest {\n [key: string]: unknown;\n}\n\ninterface AddResourceOptions {\n path?: string;\n type?: string;\n required?: boolean;\n resourceKey?: string;\n description?: string;\n permission?: string;\n fieldsJson?: string;\n dryRun?: boolean;\n}\n\nfunction loadManifest(\n pluginDir: string,\n): { manifest: ManifestWithExtras; manifestPath: string } | null {\n const resolved = resolveManifestInDir(pluginDir, { allowJsManifest: true });\n\n if (!resolved) {\n console.error(\n `No manifest found in ${pluginDir}. This command requires manifest.json (manifest.js cannot be edited in place).`,\n );\n console.error(\n \" appkit plugin add-resource --path <dir-with-manifest.json>\",\n );\n process.exit(1);\n }\n\n if (resolved.type !== \"json\") {\n console.error(\n `Editable manifest not found. add-resource only supports plugin directories that contain manifest.json (found ${path.basename(resolved.path)}).`,\n );\n process.exit(1);\n }\n\n const manifestPath = resolved.path;\n\n try {\n const raw = fs.readFileSync(manifestPath, \"utf-8\");\n const parsed = JSON.parse(raw) as unknown;\n const result = validateManifest(parsed);\n if (!result.valid || !result.manifest) {\n console.error(\n \"Invalid manifest. Run `appkit plugin validate` for details.\",\n );\n process.exit(1);\n }\n return { manifest: parsed as ManifestWithExtras, manifestPath };\n } catch (err) {\n console.error(\n \"Failed to read or parse manifest.json:\",\n err instanceof Error ? err.message : err,\n );\n process.exit(1);\n }\n}\n\nfunction buildEntry(\n type: string,\n opts: AddResourceOptions,\n): { entry: ResourceRequirement; isRequired: boolean } {\n const alias = humanizeResourceType(type);\n const isRequired = opts.required !== false;\n\n let fields = getDefaultFieldsForType(type);\n if (opts.fieldsJson) {\n try {\n const parsed = JSON.parse(opts.fieldsJson) as Record<\n string,\n { env: string; description?: string }\n >;\n fields = { ...fields, ...parsed };\n } catch {\n console.error(\"Error: --fields-json must be valid JSON.\");\n console.error(\n ' Example: --fields-json \\'{\"id\":{\"env\":\"MY_WAREHOUSE_ID\"}}\\'',\n );\n process.exit(1);\n }\n }\n\n // Constructed dynamically from a string `type` and string `permission`;\n // the per-type permission tightness from the discriminated union is\n // recovered at runtime by `validateManifest` before the entry is written.\n const entry = {\n type,\n alias,\n resourceKey: opts.resourceKey ?? resourceKeyFromType(type),\n description:\n opts.description ||\n `${isRequired ? \"Required\" : \"Optional\"} for ${alias} functionality.`,\n permission:\n opts.permission ?? DEFAULT_PERMISSION_BY_TYPE[type] ?? \"CAN_VIEW\",\n fields,\n } as ResourceRequirement;\n\n return { entry, isRequired };\n}\n\nfunction runNonInteractive(opts: AddResourceOptions): void {\n const cwd = process.cwd();\n const pluginDir = path.resolve(cwd, opts.path ?? \".\");\n const loaded = loadManifest(pluginDir);\n if (!loaded) return;\n const { manifest, manifestPath } = loaded;\n\n const type = opts.type as string;\n const validTypes = getValidResourceTypes();\n if (!validTypes.includes(type)) {\n console.error(`Error: Unknown resource type \"${type}\".`);\n console.error(` Valid types: ${validTypes.join(\", \")}`);\n process.exit(1);\n }\n const { entry, isRequired } = buildEntry(type, opts);\n\n if (isRequired) {\n manifest.resources.required.push(entry);\n } else {\n manifest.resources.optional.push(entry);\n }\n\n if (opts.dryRun) {\n console.log(JSON.stringify(manifest, null, 2));\n return;\n }\n\n fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\\n`);\n console.log(\n `Added ${entry.alias} as ${isRequired ? \"required\" : \"optional\"} to ${path.relative(cwd, manifestPath)}`,\n );\n}\n\nasync function runInteractive(opts: AddResourceOptions): Promise<void> {\n intro(\"Add resource to plugin manifest\");\n\n const cwd = process.cwd();\n const pluginDir = path.resolve(cwd, opts.path ?? \".\");\n const loaded = loadManifest(pluginDir);\n if (!loaded) return;\n const { manifest, manifestPath } = loaded;\n\n const spec = await promptOneResource();\n if (!spec) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const alias = humanizeResourceType(spec.type);\n // See note on the non-interactive variant in `buildEntry` — the entry is\n // assembled from prompt strings and validated by Zod at write time.\n const entry = {\n type: spec.type,\n alias,\n resourceKey: spec.resourceKey,\n description: spec.description || `Required for ${alias} functionality.`,\n permission: spec.permission,\n fields: spec.fields,\n } as ResourceRequirement;\n\n if (spec.required) {\n manifest.resources.required.push(entry);\n } else {\n manifest.resources.optional.push(entry);\n }\n\n fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\\n`);\n\n outro(\"Resource added.\");\n console.log(\n `\\nAdded ${alias} as ${spec.required ? \"required\" : \"optional\"} to ${path.relative(cwd, manifestPath)}`,\n );\n}\n\nasync function runPluginAddResource(opts: AddResourceOptions): Promise<void> {\n if (opts.type) {\n runNonInteractive(opts);\n } else {\n await runInteractive(opts);\n }\n}\n\nexport const pluginAddResourceCommand = new Command(\"add-resource\")\n .description(\n \"Add a resource requirement to an existing plugin manifest. Overwrites manifest.json in place.\",\n )\n .option(\n \"-p, --path <dir>\",\n \"Plugin directory containing manifest.json (default: .)\",\n )\n .option(\n \"-t, --type <resource_type>\",\n \"Resource type (e.g. sql_warehouse, volume). Enables non-interactive mode.\",\n )\n .option(\"--required\", \"Mark resource as required (default: true)\", true)\n .option(\"--no-required\", \"Mark resource as optional\")\n .option(\"--resource-key <key>\", \"Resource key (default: derived from type)\")\n .option(\"--description <text>\", \"Description of the resource requirement\")\n .option(\"--permission <perm>\", \"Permission level (default: from schema)\")\n .option(\n \"--fields-json <json>\",\n 'JSON object overriding field env vars (e.g. \\'{\"id\":{\"env\":\"MY_WAREHOUSE_ID\"}}\\')',\n )\n .option(\"--dry-run\", \"Preview the updated manifest without writing\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit plugin add-resource\n $ appkit plugin add-resource --path plugins/my-plugin --type sql_warehouse\n $ appkit plugin add-resource --path plugins/my-plugin --type volume --no-required --dry-run\n $ appkit plugin add-resource --type sql_warehouse --fields-json '{\"id\":{\"env\":\"MY_WAREHOUSE_ID\"}}'`,\n )\n .action((opts) =>\n runPluginAddResource(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":"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"}
@@ -3,7 +3,8 @@ import { getResourceTypeOptions, getResourceTypePermissions } from "../schema-re
3
3
  //#region src/cli/commands/plugin/create/resource-defaults.ts
4
4
  /**
5
5
  * Resource type and permission defaults for plugin scaffolding.
6
- * Values are derived from plugin-manifest.schema.json via schema-resources.
6
+ * Values are sourced from the canonical Zod schemas in `schemas/manifest`,
7
+ * surfaced through the `schema-resources` module.
7
8
  */
8
9
  const MANIFEST_SCHEMA_ID = "https://databricks.github.io/appkit/schemas/plugin-manifest.schema.json";
9
10
  /** Resource types from schema resourceType enum (value, human label). */
@@ -1 +1 @@
1
- {"version":3,"file":"resource-defaults.js","names":[],"sources":["../../../../../src/cli/commands/plugin/create/resource-defaults.ts"],"sourcesContent":["/**\n * Resource type and permission defaults for plugin scaffolding.\n * Values are derived from plugin-manifest.schema.json via schema-resources.\n */\n\nimport {\n getResourceTypeOptions,\n getResourceTypePermissions,\n type ResourceTypeOption,\n} from \"../schema-resources\";\n\nexport const MANIFEST_SCHEMA_ID =\n \"https://databricks.github.io/appkit/schemas/plugin-manifest.schema.json\";\n\nexport type { ResourceTypeOption };\n\n/** Resource types from schema resourceType enum (value, human label). */\nexport const RESOURCE_TYPE_OPTIONS: ResourceTypeOption[] =\n getResourceTypeOptions();\n\n/** All valid permissions per resource type, from schema allOf/if-then rules. */\nexport const PERMISSIONS_BY_TYPE: Record<string, string[]> =\n getResourceTypePermissions();\n\n/** Default (first) permission per resource type for scaffolding. */\nexport const DEFAULT_PERMISSION_BY_TYPE: Record<string, string> =\n Object.fromEntries(\n Object.entries(PERMISSIONS_BY_TYPE).map(([type, perms]) => [\n type,\n perms[0],\n ]),\n );\n\n/** Default fields per resource type: field key -> { env, description }. */\nexport const DEFAULT_FIELDS_BY_TYPE: Record<\n string,\n Record<string, { env: string; description?: string }>\n> = {\n sql_warehouse: {\n id: { env: \"DATABRICKS_WAREHOUSE_ID\", description: \"SQL Warehouse ID\" },\n },\n secret: {\n scope: { env: \"SECRET_SCOPE\", description: \"Secret scope name\" },\n key: { env: \"SECRET_KEY\", description: \"Secret key\" },\n },\n job: {\n id: { env: \"DATABRICKS_JOB_ID\", description: \"Job ID\" },\n },\n serving_endpoint: {\n id: {\n env: \"DATABRICKS_SERVING_ENDPOINT_ID\",\n description: \"Serving endpoint ID\",\n },\n },\n volume: {\n name: { env: \"VOLUME_NAME\", description: \"Volume name\" },\n },\n vector_search_index: {\n endpoint_name: {\n env: \"VECTOR_SEARCH_ENDPOINT_NAME\",\n description: \"Vector search endpoint name\",\n },\n index_name: {\n env: \"VECTOR_SEARCH_INDEX_NAME\",\n description: \"Vector search index name\",\n },\n },\n uc_function: {\n name: {\n env: \"UC_FUNCTION_NAME\",\n description: \"Unity Catalog function name\",\n },\n },\n uc_connection: {\n name: {\n env: \"UC_CONNECTION_NAME\",\n description: \"Unity Catalog connection name\",\n },\n },\n database: {\n instance_name: {\n env: \"DATABRICKS_INSTANCE_NAME\",\n description: \"Databricks instance name\",\n },\n database_name: {\n env: \"DATABASE_NAME\",\n description: \"Database name\",\n },\n },\n genie_space: {\n id: { env: \"GENIE_SPACE_ID\", description: \"Genie Space ID\" },\n },\n experiment: {\n id: { env: \"MLFLOW_EXPERIMENT_ID\", description: \"MLflow experiment ID\" },\n },\n app: {\n id: { env: \"DATABRICKS_APP_ID\", description: \"Databricks App ID\" },\n },\n};\n\n/** Valid resource type values from the schema. */\nexport function getValidResourceTypes(): string[] {\n return RESOURCE_TYPE_OPTIONS.map((o) => o.value);\n}\n\n/** Humanized alias from resource type (e.g. sql_warehouse -> \"SQL Warehouse\"). */\nexport function humanizeResourceType(type: string): string {\n const option = RESOURCE_TYPE_OPTIONS.find((o) => o.value === type);\n return option ? option.label : type.replace(/_/g, \" \");\n}\n\n/** Kebab-case resource key from type (e.g. sql_warehouse -> \"sql-warehouse\"). */\nexport function resourceKeyFromType(type: string): string {\n return type.replace(/_/g, \"-\");\n}\n\n/** Get default fields for a resource type; fallback to single id field. */\nexport function getDefaultFieldsForType(\n type: string,\n): Record<string, { env: string; description?: string }> {\n const known = DEFAULT_FIELDS_BY_TYPE[type];\n if (known) return known;\n const key = resourceKeyFromType(type);\n const envName = `DATABRICKS_${key.toUpperCase().replace(/-/g, \"_\")}_ID`;\n return {\n id: { env: envName, description: `${humanizeResourceType(type)} ID` },\n };\n}\n"],"mappings":";;;;;;;AAWA,MAAa,qBACX;;AAKF,MAAa,wBACX,wBAAwB;;AAG1B,MAAa,sBACX,4BAA4B;;AAG9B,MAAa,6BACX,OAAO,YACL,OAAO,QAAQ,oBAAoB,CAAC,KAAK,CAAC,MAAM,WAAW,CACzD,MACA,MAAM,GACP,CAAC,CACH;;AAGH,MAAa,yBAGT;CACF,eAAe,EACb,IAAI;EAAE,KAAK;EAA2B,aAAa;EAAoB,EACxE;CACD,QAAQ;EACN,OAAO;GAAE,KAAK;GAAgB,aAAa;GAAqB;EAChE,KAAK;GAAE,KAAK;GAAc,aAAa;GAAc;EACtD;CACD,KAAK,EACH,IAAI;EAAE,KAAK;EAAqB,aAAa;EAAU,EACxD;CACD,kBAAkB,EAChB,IAAI;EACF,KAAK;EACL,aAAa;EACd,EACF;CACD,QAAQ,EACN,MAAM;EAAE,KAAK;EAAe,aAAa;EAAe,EACzD;CACD,qBAAqB;EACnB,eAAe;GACb,KAAK;GACL,aAAa;GACd;EACD,YAAY;GACV,KAAK;GACL,aAAa;GACd;EACF;CACD,aAAa,EACX,MAAM;EACJ,KAAK;EACL,aAAa;EACd,EACF;CACD,eAAe,EACb,MAAM;EACJ,KAAK;EACL,aAAa;EACd,EACF;CACD,UAAU;EACR,eAAe;GACb,KAAK;GACL,aAAa;GACd;EACD,eAAe;GACb,KAAK;GACL,aAAa;GACd;EACF;CACD,aAAa,EACX,IAAI;EAAE,KAAK;EAAkB,aAAa;EAAkB,EAC7D;CACD,YAAY,EACV,IAAI;EAAE,KAAK;EAAwB,aAAa;EAAwB,EACzE;CACD,KAAK,EACH,IAAI;EAAE,KAAK;EAAqB,aAAa;EAAqB,EACnE;CACF;;AAGD,SAAgB,wBAAkC;AAChD,QAAO,sBAAsB,KAAK,MAAM,EAAE,MAAM;;;AAIlD,SAAgB,qBAAqB,MAAsB;CACzD,MAAM,SAAS,sBAAsB,MAAM,MAAM,EAAE,UAAU,KAAK;AAClE,QAAO,SAAS,OAAO,QAAQ,KAAK,QAAQ,MAAM,IAAI;;;AAIxD,SAAgB,oBAAoB,MAAsB;AACxD,QAAO,KAAK,QAAQ,MAAM,IAAI;;;AAIhC,SAAgB,wBACd,MACuD;CACvD,MAAM,QAAQ,uBAAuB;AACrC,KAAI,MAAO,QAAO;AAGlB,QAAO,EACL,IAAI;EAAE,KAFQ,cADJ,oBAAoB,KAAK,CACH,aAAa,CAAC,QAAQ,MAAM,IAAI,CAAC;EAE7C,aAAa,GAAG,qBAAqB,KAAK,CAAC;EAAM,EACtE"}
1
+ {"version":3,"file":"resource-defaults.js","names":[],"sources":["../../../../../src/cli/commands/plugin/create/resource-defaults.ts"],"sourcesContent":["/**\n * Resource type and permission defaults for plugin scaffolding.\n * Values are sourced from the canonical Zod schemas in `schemas/manifest`,\n * surfaced through the `schema-resources` module.\n */\n\nimport {\n getResourceTypeOptions,\n getResourceTypePermissions,\n type ResourceTypeOption,\n} from \"../schema-resources\";\n\nexport const MANIFEST_SCHEMA_ID =\n \"https://databricks.github.io/appkit/schemas/plugin-manifest.schema.json\";\n\nexport type { ResourceTypeOption };\n\n/** Resource types from schema resourceType enum (value, human label). */\nexport const RESOURCE_TYPE_OPTIONS: ResourceTypeOption[] =\n getResourceTypeOptions();\n\n/** All valid permissions per resource type, from schema allOf/if-then rules. */\nexport const PERMISSIONS_BY_TYPE: Record<string, string[]> =\n getResourceTypePermissions();\n\n/** Default (first) permission per resource type for scaffolding. */\nexport const DEFAULT_PERMISSION_BY_TYPE: Record<string, string> =\n Object.fromEntries(\n Object.entries(PERMISSIONS_BY_TYPE).map(([type, perms]) => [\n type,\n perms[0],\n ]),\n );\n\n/** Default fields per resource type: field key -> { env, description }. */\nexport const DEFAULT_FIELDS_BY_TYPE: Record<\n string,\n Record<string, { env: string; description?: string }>\n> = {\n sql_warehouse: {\n id: { env: \"DATABRICKS_WAREHOUSE_ID\", description: \"SQL Warehouse ID\" },\n },\n secret: {\n scope: { env: \"SECRET_SCOPE\", description: \"Secret scope name\" },\n key: { env: \"SECRET_KEY\", description: \"Secret key\" },\n },\n job: {\n id: { env: \"DATABRICKS_JOB_ID\", description: \"Job ID\" },\n },\n serving_endpoint: {\n id: {\n env: \"DATABRICKS_SERVING_ENDPOINT_ID\",\n description: \"Serving endpoint ID\",\n },\n },\n volume: {\n name: { env: \"VOLUME_NAME\", description: \"Volume name\" },\n },\n vector_search_index: {\n endpoint_name: {\n env: \"VECTOR_SEARCH_ENDPOINT_NAME\",\n description: \"Vector search endpoint name\",\n },\n index_name: {\n env: \"VECTOR_SEARCH_INDEX_NAME\",\n description: \"Vector search index name\",\n },\n },\n uc_function: {\n name: {\n env: \"UC_FUNCTION_NAME\",\n description: \"Unity Catalog function name\",\n },\n },\n uc_connection: {\n name: {\n env: \"UC_CONNECTION_NAME\",\n description: \"Unity Catalog connection name\",\n },\n },\n database: {\n instance_name: {\n env: \"DATABRICKS_INSTANCE_NAME\",\n description: \"Databricks instance name\",\n },\n database_name: {\n env: \"DATABASE_NAME\",\n description: \"Database name\",\n },\n },\n genie_space: {\n id: { env: \"GENIE_SPACE_ID\", description: \"Genie Space ID\" },\n },\n experiment: {\n id: { env: \"MLFLOW_EXPERIMENT_ID\", description: \"MLflow experiment ID\" },\n },\n app: {\n id: { env: \"DATABRICKS_APP_ID\", description: \"Databricks App ID\" },\n },\n};\n\n/** Valid resource type values from the schema. */\nexport function getValidResourceTypes(): string[] {\n return RESOURCE_TYPE_OPTIONS.map((o) => o.value);\n}\n\n/** Humanized alias from resource type (e.g. sql_warehouse -> \"SQL Warehouse\"). */\nexport function humanizeResourceType(type: string): string {\n const option = RESOURCE_TYPE_OPTIONS.find((o) => o.value === type);\n return option ? option.label : type.replace(/_/g, \" \");\n}\n\n/** Kebab-case resource key from type (e.g. sql_warehouse -> \"sql-warehouse\"). */\nexport function resourceKeyFromType(type: string): string {\n return type.replace(/_/g, \"-\");\n}\n\n/** Get default fields for a resource type; fallback to single id field. */\nexport function getDefaultFieldsForType(\n type: string,\n): Record<string, { env: string; description?: string }> {\n const known = DEFAULT_FIELDS_BY_TYPE[type];\n if (known) return known;\n const key = resourceKeyFromType(type);\n const envName = `DATABRICKS_${key.toUpperCase().replace(/-/g, \"_\")}_ID`;\n return {\n id: { env: envName, description: `${humanizeResourceType(type)} ID` },\n };\n}\n"],"mappings":";;;;;;;;AAYA,MAAa,qBACX;;AAKF,MAAa,wBACX,wBAAwB;;AAG1B,MAAa,sBACX,4BAA4B;;AAG9B,MAAa,6BACX,OAAO,YACL,OAAO,QAAQ,oBAAoB,CAAC,KAAK,CAAC,MAAM,WAAW,CACzD,MACA,MAAM,GACP,CAAC,CACH;;AAGH,MAAa,yBAGT;CACF,eAAe,EACb,IAAI;EAAE,KAAK;EAA2B,aAAa;EAAoB,EACxE;CACD,QAAQ;EACN,OAAO;GAAE,KAAK;GAAgB,aAAa;GAAqB;EAChE,KAAK;GAAE,KAAK;GAAc,aAAa;GAAc;EACtD;CACD,KAAK,EACH,IAAI;EAAE,KAAK;EAAqB,aAAa;EAAU,EACxD;CACD,kBAAkB,EAChB,IAAI;EACF,KAAK;EACL,aAAa;EACd,EACF;CACD,QAAQ,EACN,MAAM;EAAE,KAAK;EAAe,aAAa;EAAe,EACzD;CACD,qBAAqB;EACnB,eAAe;GACb,KAAK;GACL,aAAa;GACd;EACD,YAAY;GACV,KAAK;GACL,aAAa;GACd;EACF;CACD,aAAa,EACX,MAAM;EACJ,KAAK;EACL,aAAa;EACd,EACF;CACD,eAAe,EACb,MAAM;EACJ,KAAK;EACL,aAAa;EACd,EACF;CACD,UAAU;EACR,eAAe;GACb,KAAK;GACL,aAAa;GACd;EACD,eAAe;GACb,KAAK;GACL,aAAa;GACd;EACF;CACD,aAAa,EACX,IAAI;EAAE,KAAK;EAAkB,aAAa;EAAkB,EAC7D;CACD,YAAY,EACV,IAAI;EAAE,KAAK;EAAwB,aAAa;EAAwB,EACzE;CACD,KAAK,EACH,IAAI;EAAE,KAAK;EAAqB,aAAa;EAAqB,EACnE;CACF;;AAGD,SAAgB,wBAAkC;AAChD,QAAO,sBAAsB,KAAK,MAAM,EAAE,MAAM;;;AAIlD,SAAgB,qBAAqB,MAAsB;CACzD,MAAM,SAAS,sBAAsB,MAAM,MAAM,EAAE,UAAU,KAAK;AAClE,QAAO,SAAS,OAAO,QAAQ,KAAK,QAAQ,MAAM,IAAI;;;AAIxD,SAAgB,oBAAoB,MAAsB;AACxD,QAAO,KAAK,QAAQ,MAAM,IAAI;;;AAIhC,SAAgB,wBACd,MACuD;CACvD,MAAM,QAAQ,uBAAuB;AACrC,KAAI,MAAO,QAAO;AAGlB,QAAO,EACL,IAAI;EAAE,KAFQ,cADJ,oBAAoB,KAAK,CACH,aAAa,CAAC,QAAQ,MAAM,IAAI,CAAC;EAE7C,aAAa,GAAG,qBAAqB,KAAK,CAAC;EAAM,EACtE"}
@@ -1,21 +1,14 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
1
+ import { appPermissionSchema, databasePermissionSchema, experimentPermissionSchema, genieSpacePermissionSchema, jobPermissionSchema, postgresPermissionSchema, resourceTypeSchema, secretPermissionSchema, servingEndpointPermissionSchema, sqlWarehousePermissionSchema, ucConnectionPermissionSchema, ucFunctionPermissionSchema, vectorSearchIndexPermissionSchema, volumePermissionSchema } from "../../../schemas/manifest.js";
4
2
 
5
3
  //#region src/cli/commands/plugin/schema-resources.ts
6
4
  /**
7
- * Resource types and permissions derived from plugin-manifest.schema.json.
8
- * Single source of truth so create, add-resource, and validate stay in sync with the schema.
5
+ * Resource types and permissions sourced from the canonical Zod schemas in
6
+ * `schemas/manifest.ts`. Single source of truth so create, add-resource, and
7
+ * validate stay in sync with the schema.
8
+ *
9
+ * Values are constant at module load (direct Zod imports, no runtime JSON-schema
10
+ * read), so no caching is needed.
9
11
  */
10
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
- const SCHEMA_NAME = "plugin-manifest.schema.json";
12
- const SCHEMA_PATHS = [path.join(__dirname, "..", "..", "..", "schemas", SCHEMA_NAME), path.join(__dirname, "..", "..", "schemas", SCHEMA_NAME)];
13
- function loadSchema() {
14
- for (const schemaPath of SCHEMA_PATHS) try {
15
- if (fs.existsSync(schemaPath)) return JSON.parse(fs.readFileSync(schemaPath, "utf-8"));
16
- } catch {}
17
- return null;
18
- }
19
12
  /** Optional display overrides for acronyms (e.g. SQL, UC). Omitted entries use title-case of value. */
20
13
  const LABEL_OVERRIDES = {
21
14
  sql_warehouse: "SQL Warehouse",
@@ -26,54 +19,44 @@ function humanize(value) {
26
19
  if (LABEL_OVERRIDES[value]) return LABEL_OVERRIDES[value];
27
20
  return value.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
28
21
  }
29
- let cachedOptions = null;
30
- let cachedPermissions = null;
31
22
  /**
32
- * Resource type options (value + label) from schema $defs.resourceType.enum.
23
+ * Map from resource type literal to its permission enum schema. Mirrors the
24
+ * `allOf + if/then` block in the previous hand-written JSON Schema, but driven
25
+ * directly from the Zod source. Add an entry here when extending
26
+ * `resourceTypeSchema`.
27
+ */
28
+ const PERMISSION_SCHEMAS_BY_TYPE = {
29
+ secret: secretPermissionSchema,
30
+ job: jobPermissionSchema,
31
+ sql_warehouse: sqlWarehousePermissionSchema,
32
+ serving_endpoint: servingEndpointPermissionSchema,
33
+ volume: volumePermissionSchema,
34
+ vector_search_index: vectorSearchIndexPermissionSchema,
35
+ uc_function: ucFunctionPermissionSchema,
36
+ uc_connection: ucConnectionPermissionSchema,
37
+ database: databasePermissionSchema,
38
+ postgres: postgresPermissionSchema,
39
+ genie_space: genieSpacePermissionSchema,
40
+ experiment: experimentPermissionSchema,
41
+ app: appPermissionSchema
42
+ };
43
+ /**
44
+ * Resource type options (value + label) from `resourceTypeSchema.options`.
33
45
  */
34
46
  function getResourceTypeOptions() {
35
- if (cachedOptions) return cachedOptions;
36
- const enumArr = ((loadSchema()?.$defs)?.resourceType)?.enum;
37
- if (!Array.isArray(enumArr)) {
38
- cachedOptions = [];
39
- return cachedOptions;
40
- }
41
- cachedOptions = enumArr.map((value) => ({
47
+ return resourceTypeSchema.options.map((value) => ({
42
48
  value,
43
49
  label: humanize(value)
44
50
  }));
45
- return cachedOptions;
46
51
  }
47
52
  /**
48
- * Permissions per resource type from schema resourceRequirement.allOf (if/then).
53
+ * Permissions per resource type, derived from each type's per-type permission
54
+ * schema's `.options` array. Order is weakest-to-strongest (matches Zod enum
55
+ * declaration order, which the original JSON Schema also preserved).
49
56
  */
50
57
  function getResourceTypePermissions() {
51
- if (cachedPermissions) return cachedPermissions;
52
- const schema = loadSchema();
53
58
  const out = {};
54
- if (!schema?.$defs || typeof schema.$defs !== "object") {
55
- cachedPermissions = out;
56
- return out;
57
- }
58
- const allOf = schema.$defs.resourceRequirement?.allOf;
59
- if (!Array.isArray(allOf)) {
60
- cachedPermissions = out;
61
- return out;
62
- }
63
- for (const branch of allOf) {
64
- const typeConst = branch?.if?.properties?.type?.const;
65
- const ref = branch?.then?.properties?.permission?.$ref;
66
- if (typeof typeConst !== "string" || typeof ref !== "string") continue;
67
- const refSegments = ref.replace(/^#\//, "").split("/");
68
- let def = schema;
69
- for (const seg of refSegments) {
70
- if (def == null || typeof def !== "object") break;
71
- def = def[seg];
72
- }
73
- const enumArr = Array.isArray(def?.enum) ? def.enum : void 0;
74
- if (enumArr?.length) out[typeConst] = enumArr;
75
- }
76
- cachedPermissions = out;
59
+ for (const [type, schema] of Object.entries(PERMISSION_SCHEMAS_BY_TYPE)) out[type] = [...schema.options];
77
60
  return out;
78
61
  }
79
62
 
@@ -1 +1 @@
1
- {"version":3,"file":"schema-resources.js","names":[],"sources":["../../../../src/cli/commands/plugin/schema-resources.ts"],"sourcesContent":["/**\n * Resource types and permissions derived from plugin-manifest.schema.json.\n * Single source of truth so create, add-resource, and validate stay in sync with the schema.\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst SCHEMA_NAME = \"plugin-manifest.schema.json\";\n// Try dist/schemas first (shared build + appkit pack), then dist/cli/schemas\nconst SCHEMA_PATHS = [\n path.join(__dirname, \"..\", \"..\", \"..\", \"schemas\", SCHEMA_NAME),\n path.join(__dirname, \"..\", \"..\", \"schemas\", SCHEMA_NAME),\n];\n\nexport interface ResourceTypeOption {\n value: string;\n label: string;\n}\n\nfunction loadSchema(): Record<string, unknown> | null {\n for (const schemaPath of SCHEMA_PATHS) {\n try {\n if (fs.existsSync(schemaPath)) {\n return JSON.parse(fs.readFileSync(schemaPath, \"utf-8\")) as Record<\n string,\n unknown\n >;\n }\n } catch {\n // try next path\n }\n }\n return null;\n}\n\n/** Optional display overrides for acronyms (e.g. SQL, UC). Omitted entries use title-case of value. */\nconst LABEL_OVERRIDES: Record<string, string> = {\n sql_warehouse: \"SQL Warehouse\",\n uc_function: \"UC Function\",\n uc_connection: \"UC Connection\",\n};\n\nfunction humanize(value: string): string {\n if (LABEL_OVERRIDES[value]) return LABEL_OVERRIDES[value];\n return value.replace(/_/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\nlet cachedOptions: ResourceTypeOption[] | null = null;\nlet cachedPermissions: Record<string, string[]> | null = null;\n\n/**\n * Resource type options (value + label) from schema $defs.resourceType.enum.\n */\nexport function getResourceTypeOptions(): ResourceTypeOption[] {\n if (cachedOptions) return cachedOptions;\n const schema = loadSchema();\n const defs = schema?.$defs as Record<string, unknown> | undefined;\n const resourceType = defs?.resourceType as { enum?: string[] } | undefined;\n const enumArr = resourceType?.enum;\n if (!Array.isArray(enumArr)) {\n cachedOptions = [];\n return cachedOptions;\n }\n cachedOptions = enumArr.map((value) => ({\n value,\n label: humanize(value),\n }));\n return cachedOptions;\n}\n\n/**\n * Permissions per resource type from schema resourceRequirement.allOf (if/then).\n */\nexport function getResourceTypePermissions(): Record<string, string[]> {\n if (cachedPermissions) return cachedPermissions;\n const schema = loadSchema();\n const out: Record<string, string[]> = {};\n if (!schema?.$defs || typeof schema.$defs !== \"object\") {\n cachedPermissions = out;\n return out;\n }\n const defs = schema.$defs as Record<string, unknown>;\n const resourceReq = defs.resourceRequirement as\n | Record<string, unknown>\n | undefined;\n const allOf = resourceReq?.allOf as\n | Array<{\n if?: { properties?: { type?: { const?: string } } };\n then?: { properties?: { permission?: { $ref?: string } } };\n }>\n | undefined;\n if (!Array.isArray(allOf)) {\n cachedPermissions = out;\n return out;\n }\n for (const branch of allOf) {\n const typeConst = branch?.if?.properties?.type?.const;\n const ref = branch?.then?.properties?.permission?.$ref;\n if (typeof typeConst !== \"string\" || typeof ref !== \"string\") continue;\n const refSegments = ref.replace(/^#\\//, \"\").split(\"/\");\n let def: unknown = schema;\n for (const seg of refSegments) {\n if (def == null || typeof def !== \"object\") break;\n def = (def as Record<string, unknown>)[seg];\n }\n const enumArr = Array.isArray((def as { enum?: string[] })?.enum)\n ? (def as { enum: string[] }).enum\n : undefined;\n if (enumArr?.length) out[typeConst] = enumArr;\n }\n cachedPermissions = out;\n return out;\n}\n"],"mappings":";;;;;;;;;AAQA,MAAM,YAAY,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;AAC9D,MAAM,cAAc;AAEpB,MAAM,eAAe,CACnB,KAAK,KAAK,WAAW,MAAM,MAAM,MAAM,WAAW,YAAY,EAC9D,KAAK,KAAK,WAAW,MAAM,MAAM,WAAW,YAAY,CACzD;AAOD,SAAS,aAA6C;AACpD,MAAK,MAAM,cAAc,aACvB,KAAI;AACF,MAAI,GAAG,WAAW,WAAW,CAC3B,QAAO,KAAK,MAAM,GAAG,aAAa,YAAY,QAAQ,CAAC;SAKnD;AAIV,QAAO;;;AAIT,MAAM,kBAA0C;CAC9C,eAAe;CACf,aAAa;CACb,eAAe;CAChB;AAED,SAAS,SAAS,OAAuB;AACvC,KAAI,gBAAgB,OAAQ,QAAO,gBAAgB;AACnD,QAAO,MAAM,QAAQ,MAAM,IAAI,CAAC,QAAQ,UAAU,MAAM,EAAE,aAAa,CAAC;;AAG1E,IAAI,gBAA6C;AACjD,IAAI,oBAAqD;;;;AAKzD,SAAgB,yBAA+C;AAC7D,KAAI,cAAe,QAAO;CAI1B,MAAM,YAHS,YAAY,EACN,QACM,eACG;AAC9B,KAAI,CAAC,MAAM,QAAQ,QAAQ,EAAE;AAC3B,kBAAgB,EAAE;AAClB,SAAO;;AAET,iBAAgB,QAAQ,KAAK,WAAW;EACtC;EACA,OAAO,SAAS,MAAM;EACvB,EAAE;AACH,QAAO;;;;;AAMT,SAAgB,6BAAuD;AACrE,KAAI,kBAAmB,QAAO;CAC9B,MAAM,SAAS,YAAY;CAC3B,MAAM,MAAgC,EAAE;AACxC,KAAI,CAAC,QAAQ,SAAS,OAAO,OAAO,UAAU,UAAU;AACtD,sBAAoB;AACpB,SAAO;;CAMT,MAAM,QAJO,OAAO,MACK,qBAGE;AAM3B,KAAI,CAAC,MAAM,QAAQ,MAAM,EAAE;AACzB,sBAAoB;AACpB,SAAO;;AAET,MAAK,MAAM,UAAU,OAAO;EAC1B,MAAM,YAAY,QAAQ,IAAI,YAAY,MAAM;EAChD,MAAM,MAAM,QAAQ,MAAM,YAAY,YAAY;AAClD,MAAI,OAAO,cAAc,YAAY,OAAO,QAAQ,SAAU;EAC9D,MAAM,cAAc,IAAI,QAAQ,QAAQ,GAAG,CAAC,MAAM,IAAI;EACtD,IAAI,MAAe;AACnB,OAAK,MAAM,OAAO,aAAa;AAC7B,OAAI,OAAO,QAAQ,OAAO,QAAQ,SAAU;AAC5C,SAAO,IAAgC;;EAEzC,MAAM,UAAU,MAAM,QAAS,KAA6B,KAAK,GAC5D,IAA2B,OAC5B;AACJ,MAAI,SAAS,OAAQ,KAAI,aAAa;;AAExC,qBAAoB;AACpB,QAAO"}
1
+ {"version":3,"file":"schema-resources.js","names":[],"sources":["../../../../src/cli/commands/plugin/schema-resources.ts"],"sourcesContent":["/**\n * Resource types and permissions sourced from the canonical Zod schemas in\n * `schemas/manifest.ts`. Single source of truth so create, add-resource, and\n * validate stay in sync with the schema.\n *\n * Values are constant at module load (direct Zod imports, no runtime JSON-schema\n * read), so no caching is needed.\n */\nimport {\n appPermissionSchema,\n databasePermissionSchema,\n experimentPermissionSchema,\n genieSpacePermissionSchema,\n jobPermissionSchema,\n postgresPermissionSchema,\n resourceTypeSchema,\n secretPermissionSchema,\n servingEndpointPermissionSchema,\n sqlWarehousePermissionSchema,\n ucConnectionPermissionSchema,\n ucFunctionPermissionSchema,\n vectorSearchIndexPermissionSchema,\n volumePermissionSchema,\n} from \"../../../schemas/manifest\";\n\nexport interface ResourceTypeOption {\n value: string;\n label: string;\n}\n\n/** Optional display overrides for acronyms (e.g. SQL, UC). Omitted entries use title-case of value. */\nconst LABEL_OVERRIDES: Record<string, string> = {\n sql_warehouse: \"SQL Warehouse\",\n uc_function: \"UC Function\",\n uc_connection: \"UC Connection\",\n};\n\nfunction humanize(value: string): string {\n if (LABEL_OVERRIDES[value]) return LABEL_OVERRIDES[value];\n return value.replace(/_/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\n/**\n * Map from resource type literal to its permission enum schema. Mirrors the\n * `allOf + if/then` block in the previous hand-written JSON Schema, but driven\n * directly from the Zod source. Add an entry here when extending\n * `resourceTypeSchema`.\n */\nconst PERMISSION_SCHEMAS_BY_TYPE = {\n secret: secretPermissionSchema,\n job: jobPermissionSchema,\n sql_warehouse: sqlWarehousePermissionSchema,\n serving_endpoint: servingEndpointPermissionSchema,\n volume: volumePermissionSchema,\n vector_search_index: vectorSearchIndexPermissionSchema,\n uc_function: ucFunctionPermissionSchema,\n uc_connection: ucConnectionPermissionSchema,\n database: databasePermissionSchema,\n postgres: postgresPermissionSchema,\n genie_space: genieSpacePermissionSchema,\n experiment: experimentPermissionSchema,\n app: appPermissionSchema,\n} as const;\n\n/**\n * Resource type options (value + label) from `resourceTypeSchema.options`.\n */\nexport function getResourceTypeOptions(): ResourceTypeOption[] {\n return resourceTypeSchema.options.map((value) => ({\n value,\n label: humanize(value),\n }));\n}\n\n/**\n * Permissions per resource type, derived from each type's per-type permission\n * schema's `.options` array. Order is weakest-to-strongest (matches Zod enum\n * declaration order, which the original JSON Schema also preserved).\n */\nexport function getResourceTypePermissions(): Record<string, string[]> {\n const out: Record<string, string[]> = {};\n for (const [type, schema] of Object.entries(PERMISSION_SCHEMAS_BY_TYPE)) {\n out[type] = [...schema.options];\n }\n return out;\n}\n"],"mappings":";;;;;;;;;;;;AA+BA,MAAM,kBAA0C;CAC9C,eAAe;CACf,aAAa;CACb,eAAe;CAChB;AAED,SAAS,SAAS,OAAuB;AACvC,KAAI,gBAAgB,OAAQ,QAAO,gBAAgB;AACnD,QAAO,MAAM,QAAQ,MAAM,IAAI,CAAC,QAAQ,UAAU,MAAM,EAAE,aAAa,CAAC;;;;;;;;AAS1E,MAAM,6BAA6B;CACjC,QAAQ;CACR,KAAK;CACL,eAAe;CACf,kBAAkB;CAClB,QAAQ;CACR,qBAAqB;CACrB,aAAa;CACb,eAAe;CACf,UAAU;CACV,UAAU;CACV,aAAa;CACb,YAAY;CACZ,KAAK;CACN;;;;AAKD,SAAgB,yBAA+C;AAC7D,QAAO,mBAAmB,QAAQ,KAAK,WAAW;EAChD;EACA,OAAO,SAAS,MAAM;EACvB,EAAE;;;;;;;AAQL,SAAgB,6BAAuD;CACrE,MAAM,MAAgC,EAAE;AACxC,MAAK,MAAM,CAAC,MAAM,WAAW,OAAO,QAAQ,2BAA2B,CACrE,KAAI,QAAQ,CAAC,GAAG,OAAO,QAAQ;AAEjC,QAAO"}
@@ -1,3 +1,4 @@
1
+ import { TEMPLATE_SCAFFOLDING, templateFieldEntrySchema } from "../../../../schemas/manifest.js";
1
2
  import { loadManifestFromFile, resolveManifestInDir } from "../manifest-resolve.js";
2
3
  import { formatValidationErrors, validateManifest } from "../validate/validate-manifest.js";
3
4
  import { shouldAllowJsManifestForPackage } from "../trusted-js-manifest.js";
@@ -21,13 +22,13 @@ function isWithinDirectory(filePath, boundary) {
21
22
  return resolvedPath === resolvedBoundary || resolvedPath.startsWith(`${resolvedBoundary}${path.sep}`);
22
23
  }
23
24
  /**
24
- * Validates a parsed JSON object against the plugin-manifest JSON schema.
25
+ * Validates a parsed JSON object against the plugin-manifest schema.
25
26
  * Returns the manifest if valid, or null and logs schema errors.
26
27
  */
27
28
  function validateManifestWithSchema(obj, sourcePath) {
28
29
  const result = validateManifest(obj);
29
30
  if (result.valid && result.manifest) return result.manifest;
30
- if (result.errors?.length) console.warn(`Warning: Manifest at ${sourcePath} failed schema validation:\n${formatValidationErrors(result.errors, obj)}`);
31
+ if (result.errors?.length) console.warn(`Warning: Manifest at ${sourcePath} failed schema validation:\n${formatValidationErrors(result.errors)}`);
31
32
  return null;
32
33
  }
33
34
  /** Safety limit for recursive directory scanning to prevent runaway traversal. */
@@ -47,7 +48,8 @@ async function loadPluginEntry(resolved, pkg, allowJsManifest) {
47
48
  package: pkg,
48
49
  resources: manifest.resources,
49
50
  ...manifest.onSetupMessage && { onSetupMessage: manifest.onSetupMessage },
50
- ...manifest.stability && manifest.stability !== "ga" && { stability: manifest.stability }
51
+ ...manifest.stability && manifest.stability !== "ga" && { stability: manifest.stability },
52
+ ...manifest.scaffolding && { scaffolding: manifest.scaffolding }
51
53
  }];
52
54
  }
53
55
  /**
@@ -256,7 +258,8 @@ async function scanForPlugins(cwd, packages, allowJsManifest) {
256
258
  package: packageName,
257
259
  resources: manifest.resources,
258
260
  ...manifest.onSetupMessage && { onSetupMessage: manifest.onSetupMessage },
259
- ...manifest.stability && manifest.stability !== "ga" && { stability: manifest.stability }
261
+ ...manifest.stability && manifest.stability !== "ga" && { stability: manifest.stability },
262
+ ...manifest.scaffolding && { scaffolding: manifest.scaffolding }
260
263
  };
261
264
  }
262
265
  }
@@ -321,12 +324,23 @@ async function scanPluginsDir(dir, packageName, allowJsManifest, cwd) {
321
324
  }
322
325
  /**
323
326
  * Write (or preview) the template plugins manifest to disk.
327
+ *
328
+ * Each resource field is parsed through `templateFieldEntrySchema` so the
329
+ * `origin` transform fires and produces canonical `origin` values, even
330
+ * when the input carries a stale `origin`. Parsing per-field (rather than
331
+ * the whole manifest) keeps the surrounding plugin/resource key order
332
+ * stable so the synced JSON's diff stays minimal.
324
333
  */
325
334
  function writeManifest(outputPath, { plugins }, options) {
335
+ for (const plugin of Object.values(plugins)) for (const group of [plugin.resources.required, plugin.resources.optional]) for (const resource of group) {
336
+ if (!resource.fields) continue;
337
+ for (const fieldName of Object.keys(resource.fields)) resource.fields[fieldName] = templateFieldEntrySchema.parse(resource.fields[fieldName]);
338
+ }
326
339
  const templateManifest = {
327
340
  $schema: "https://databricks.github.io/appkit/schemas/template-plugins.schema.json",
328
- version: "1.1",
329
- plugins
341
+ version: "2.0",
342
+ plugins,
343
+ scaffolding: TEMPLATE_SCAFFOLDING
330
344
  };
331
345
  const serialized = JSON.stringify(templateManifest, null, 2);
332
346
  if (options.json) console.log(serialized);