@databricks/appkit 0.61.1 → 0.63.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 (194) hide show
  1. package/CLAUDE.md +1 -0
  2. package/dist/agents/databricks.d.ts.map +1 -1
  3. package/dist/agents/databricks.js.map +1 -1
  4. package/dist/agents/supervisor-api.d.ts.map +1 -1
  5. package/dist/agents/supervisor-api.js.map +1 -1
  6. package/dist/app/index.d.ts.map +1 -1
  7. package/dist/app/index.js.map +1 -1
  8. package/dist/appkit/package.js +1 -1
  9. package/dist/cache/index.d.ts.map +1 -1
  10. package/dist/cache/index.js.map +1 -1
  11. package/dist/cache/storage/memory.js.map +1 -1
  12. package/dist/cache/storage/persistent.js.map +1 -1
  13. package/dist/cli/commands/codemod/index.js.map +1 -1
  14. package/dist/cli/commands/codemod/on-plugins-ready.js.map +1 -1
  15. package/dist/cli/commands/docs.js.map +1 -1
  16. package/dist/cli/commands/doctor/bundle.js.map +1 -1
  17. package/dist/cli/commands/doctor/index.js.map +1 -1
  18. package/dist/cli/commands/doctor/report.js.map +1 -1
  19. package/dist/cli/commands/doctor/resolve-targets.js.map +1 -1
  20. package/dist/cli/commands/doctor/run.js.map +1 -1
  21. package/dist/cli/commands/generate-types.js.map +1 -1
  22. package/dist/cli/commands/lint.js.map +1 -1
  23. package/dist/cli/commands/plugin/add-resource/add-resource.js.map +1 -1
  24. package/dist/cli/commands/plugin/create/create.js.map +1 -1
  25. package/dist/cli/commands/plugin/create/prompt-resource.js.map +1 -1
  26. package/dist/cli/commands/plugin/create/scaffold.js.map +1 -1
  27. package/dist/cli/commands/plugin/index.js.map +1 -1
  28. package/dist/cli/commands/plugin/list/list.js.map +1 -1
  29. package/dist/cli/commands/plugin/promote/promote.js.map +1 -1
  30. package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
  31. package/dist/cli/commands/plugin/validate/validate-manifest.js.map +1 -1
  32. package/dist/cli/commands/plugin/validate/validate.js.map +1 -1
  33. package/dist/cli/commands/registry/add.js.map +1 -1
  34. package/dist/cli/commands/registry/client.js.map +1 -1
  35. package/dist/cli/commands/registry/config-writer.js.map +1 -1
  36. package/dist/cli/commands/registry/env-reconcile.js.map +1 -1
  37. package/dist/cli/commands/registry/env-writer.js.map +1 -1
  38. package/dist/cli/commands/registry/index.js.map +1 -1
  39. package/dist/cli/commands/registry/info.js.map +1 -1
  40. package/dist/cli/commands/registry/list.js.map +1 -1
  41. package/dist/cli/commands/registry/requirements.js.map +1 -1
  42. package/dist/cli/commands/registry/server-register.js.map +1 -1
  43. package/dist/cli/commands/registry/workspace-picker.js.map +1 -1
  44. package/dist/cli/commands/setup.js.map +1 -1
  45. package/dist/cli/index.js.map +1 -1
  46. package/dist/connectors/files/client.js.map +1 -1
  47. package/dist/connectors/lakebase/index.d.ts.map +1 -1
  48. package/dist/connectors/lakebase/index.js.map +1 -1
  49. package/dist/connectors/lakebase/pool-manager.d.ts.map +1 -1
  50. package/dist/connectors/lakebase/pool-manager.js.map +1 -1
  51. package/dist/connectors/lakebase/routing-pool.d.ts.map +1 -1
  52. package/dist/connectors/lakebase/routing-pool.js.map +1 -1
  53. package/dist/connectors/mcp/client.d.ts.map +1 -1
  54. package/dist/connectors/mcp/client.js.map +1 -1
  55. package/dist/connectors/sql-warehouse/client.js.map +1 -1
  56. package/dist/context/client-options.js.map +1 -1
  57. package/dist/context/execution-context.d.ts.map +1 -1
  58. package/dist/context/execution-context.js.map +1 -1
  59. package/dist/context/index.d.ts +1 -1
  60. package/dist/context/service-context.d.ts +55 -3
  61. package/dist/context/service-context.d.ts.map +1 -1
  62. package/dist/context/service-context.js.map +1 -1
  63. package/dist/core/agent/build-toolkit.js.map +1 -1
  64. package/dist/core/agent/load-agents.d.ts.map +1 -1
  65. package/dist/core/agent/load-agents.js.map +1 -1
  66. package/dist/core/agent/run-agent.d.ts.map +1 -1
  67. package/dist/core/agent/run-agent.js.map +1 -1
  68. package/dist/core/agent/toolkit-resolver.js.map +1 -1
  69. package/dist/core/agent/tools/define-tool.d.ts.map +1 -1
  70. package/dist/core/agent/tools/define-tool.js.map +1 -1
  71. package/dist/core/agent/tools/tool.d.ts.map +1 -1
  72. package/dist/core/agent/tools/tool.js.map +1 -1
  73. package/dist/core/agent/types.d.ts.map +1 -1
  74. package/dist/core/agent/types.js.map +1 -1
  75. package/dist/core/appkit.d.ts.map +1 -1
  76. package/dist/core/appkit.js.map +1 -1
  77. package/dist/core/lifecycle-manager.js.map +1 -1
  78. package/dist/core/plugin-context.d.ts +13 -0
  79. package/dist/core/plugin-context.d.ts.map +1 -1
  80. package/dist/core/plugin-context.js +12 -1
  81. package/dist/core/plugin-context.js.map +1 -1
  82. package/dist/errors/configuration.d.ts.map +1 -1
  83. package/dist/errors/configuration.js.map +1 -1
  84. package/dist/logging/logger.js.map +1 -1
  85. package/dist/logging/wide-event-emitter.js.map +1 -1
  86. package/dist/plugin/dev-reader.d.ts.map +1 -1
  87. package/dist/plugin/dev-reader.js.map +1 -1
  88. package/dist/plugin/interceptors/cache.js.map +1 -1
  89. package/dist/plugin/interceptors/retry.js.map +1 -1
  90. package/dist/plugin/interceptors/telemetry.js.map +1 -1
  91. package/dist/plugin/plugin.d.ts +2 -2
  92. package/dist/plugin/plugin.d.ts.map +1 -1
  93. package/dist/plugin/plugin.js.map +1 -1
  94. package/dist/plugins/agents/agents.d.ts.map +1 -1
  95. package/dist/plugins/agents/agents.js.map +1 -1
  96. package/dist/plugins/agents/event-translator.js.map +1 -1
  97. package/dist/plugins/agents/thread-store.js.map +1 -1
  98. package/dist/plugins/ai-search/ai-search.d.ts.map +1 -1
  99. package/dist/plugins/ai-search/ai-search.js.map +1 -1
  100. package/dist/plugins/analytics/analytics.d.ts.map +1 -1
  101. package/dist/plugins/analytics/analytics.js.map +1 -1
  102. package/dist/plugins/analytics/mv/cache.js.map +1 -1
  103. package/dist/plugins/analytics/mv/constants.js.map +1 -1
  104. package/dist/plugins/analytics/mv/formatters.js.map +1 -1
  105. package/dist/plugins/analytics/mv/metadata.js.map +1 -1
  106. package/dist/plugins/analytics/mv/registry.js.map +1 -1
  107. package/dist/plugins/analytics/mv/schemas.js.map +1 -1
  108. package/dist/plugins/analytics/query.js.map +1 -1
  109. package/dist/plugins/analytics/result-delivery.js.map +1 -1
  110. package/dist/plugins/files/helpers.js.map +1 -1
  111. package/dist/plugins/files/plugin.d.ts.map +1 -1
  112. package/dist/plugins/files/plugin.js.map +1 -1
  113. package/dist/plugins/files/types.d.ts.map +1 -1
  114. package/dist/plugins/genie/genie.d.ts.map +1 -1
  115. package/dist/plugins/genie/genie.js.map +1 -1
  116. package/dist/plugins/jobs/plugin.d.ts.map +1 -1
  117. package/dist/plugins/jobs/plugin.js.map +1 -1
  118. package/dist/plugins/jobs/types.d.ts.map +1 -1
  119. package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
  120. package/dist/plugins/lakebase/lakebase.js.map +1 -1
  121. package/dist/plugins/lakebase/types.d.ts.map +1 -1
  122. package/dist/plugins/server/base-server.js.map +1 -1
  123. package/dist/plugins/server/client-config-sanitizer.js.map +1 -1
  124. package/dist/plugins/server/index.d.ts.map +1 -1
  125. package/dist/plugins/server/index.js.map +1 -1
  126. package/dist/plugins/server/react-source-loc-vite-plugin.js.map +1 -1
  127. package/dist/plugins/server/remote-tunnel/remote-tunnel-controller.js.map +1 -1
  128. package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
  129. package/dist/plugins/server/static-server.js.map +1 -1
  130. package/dist/plugins/server/utils.js.map +1 -1
  131. package/dist/plugins/server/vite-dev-server.js.map +1 -1
  132. package/dist/plugins/serving/schema-filter.js.map +1 -1
  133. package/dist/plugins/serving/serving.d.ts.map +1 -1
  134. package/dist/plugins/serving/serving.js.map +1 -1
  135. package/dist/plugins/ui-variants/index.js.map +1 -1
  136. package/dist/registry/manifest-loader.d.ts.map +1 -1
  137. package/dist/registry/manifest-loader.js.map +1 -1
  138. package/dist/registry/resource-registry.d.ts.map +1 -1
  139. package/dist/registry/resource-registry.js.map +1 -1
  140. package/dist/registry/types.d.ts.map +1 -1
  141. package/dist/registry/types.js.map +1 -1
  142. package/dist/schemas/manifest.d.ts.map +1 -1
  143. package/dist/schemas/manifest.js.map +1 -1
  144. package/dist/schemas/metric-fqn.js.map +1 -1
  145. package/dist/shared/src/plugin.d.ts.map +1 -1
  146. package/dist/shared/src/schemas/manifest.d.ts.map +1 -1
  147. package/dist/shared/src/schemas/metric-fqn.js.map +1 -1
  148. package/dist/shared/src/schemas/metric-metadata-bundle.js.map +1 -1
  149. package/dist/shared/src/schemas/metric-source.js.map +1 -1
  150. package/dist/shared/src/sse/analytics.js.map +1 -1
  151. package/dist/shared/src/workspace-client/legacy.js.map +1 -1
  152. package/dist/shared/src/workspace-client/types.d.ts.map +1 -1
  153. package/dist/stream/sse-writer.js.map +1 -1
  154. package/dist/stream/stream-manager.d.ts.map +1 -1
  155. package/dist/stream/stream-manager.js.map +1 -1
  156. package/dist/stream/types.js.map +1 -1
  157. package/dist/telemetry/instrumentations.js.map +1 -1
  158. package/dist/telemetry/telemetry-manager.js.map +1 -1
  159. package/dist/telemetry/telemetry-provider.js.map +1 -1
  160. package/dist/telemetry/trace-sampler.js.map +1 -1
  161. package/dist/testing/expect-stream.d.ts +111 -0
  162. package/dist/testing/expect-stream.d.ts.map +1 -0
  163. package/dist/testing/expect-stream.js +154 -0
  164. package/dist/testing/expect-stream.js.map +1 -0
  165. package/dist/testing/fixtures.d.ts +257 -0
  166. package/dist/testing/fixtures.d.ts.map +1 -0
  167. package/dist/testing/fixtures.js +385 -0
  168. package/dist/testing/fixtures.js.map +1 -0
  169. package/dist/testing/index.d.ts +5 -0
  170. package/dist/testing/index.js +5 -0
  171. package/dist/testing/test-plugin-context.d.ts +158 -0
  172. package/dist/testing/test-plugin-context.d.ts.map +1 -0
  173. package/dist/testing/test-plugin-context.js +139 -0
  174. package/dist/testing/test-plugin-context.js.map +1 -0
  175. package/dist/type-generator/cache.js.map +1 -1
  176. package/dist/type-generator/index.js.map +1 -1
  177. package/dist/type-generator/migration.js.map +1 -1
  178. package/dist/type-generator/mv-registry/config.js.map +1 -1
  179. package/dist/type-generator/query-registry.js.map +1 -1
  180. package/dist/type-generator/serving/cache.js.map +1 -1
  181. package/dist/type-generator/serving/generator.js.map +1 -1
  182. package/dist/type-generator/serving/server-file-extractor.d.ts.map +1 -1
  183. package/dist/type-generator/serving/server-file-extractor.js.map +1 -1
  184. package/dist/type-generator/serving/vite-plugin.d.ts.map +1 -1
  185. package/dist/type-generator/serving/vite-plugin.js.map +1 -1
  186. package/dist/type-generator/vite-plugin.d.ts.map +1 -1
  187. package/dist/type-generator/vite-plugin.js.map +1 -1
  188. package/dist/workspace-client/legacy.js.map +1 -1
  189. package/dist/workspace-client/types.d.ts.map +1 -1
  190. package/docs/plugins/testing.md +245 -0
  191. package/llms.txt +1 -0
  192. package/package.json +15 -2
  193. package/sbom.cdx.json +1 -1
  194. package/skills/appkit-ui-variants/SKILL.md +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/cache/index.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\nimport { createLakebasePool } from \"../connectors/lakebase\";\nimport { getClientOptions } from \"../context/client-options\";\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 { ApiError, createWorkspaceClient } from \"../workspace-client\";\nimport { cacheDefaults } from \"./defaults\";\nimport { InMemoryStorage, PersistentStorage } from \"./storage\";\n\nconst logger = createLogger(\"cache\");\n\n/**\n * Reference-counted in-flight cache execution entry.\n *\n * `sharedController` decouples the cached `fn()` from any single caller's\n * abort signal. Callers join an in-flight entry by incrementing `refCount`;\n * when a caller aborts, refCount is decremented. The shared controller is\n * aborted only when refCount drops to 0 — i.e. all callers have abandoned\n * the request. This prevents one caller's cancellation (e.g. React\n * StrictMode unmount) from poisoning the in-flight result for other still-\n * connected awaiters.\n */\ninterface InFlightEntry<T> {\n promise: Promise<T>;\n refCount: number;\n sharedController: AbortController;\n abortTimer?: ReturnType<typeof setTimeout>;\n}\n\nfunction createAbortError(signal: AbortSignal): unknown {\n if (signal.reason !== undefined) return signal.reason;\n return new DOMException(\"The operation was aborted.\", \"AbortError\");\n}\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 static readonly ABORT_GRACE_PERIOD_MS = 100;\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, InFlightEntry<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 = createWorkspaceClient({\n clientOptions: getClientOptions(),\n }).toLegacyWorkspaceClient();\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 *\n * Multiple concurrent callers with the same `cacheKey` are deduplicated\n * onto a single in-flight execution. Each caller may pass its own\n * `callerSignal`; the underlying `fn()` is run with a shared, internally\n * managed `AbortSignal` that aborts only when *all* callers have\n * abandoned the request (reference counted). This decouples a single\n * caller's cancellation (e.g. React StrictMode unmount) from the shared\n * result, so other still-connected callers receive the cached value\n * normally.\n *\n * @param key - Cache key parts\n * @param fn - Function to execute. Receives the cache-owned shared signal;\n * pass it through to the underlying I/O so the work is cancelled when\n * no caller is left waiting.\n * @param userKey - User key for cache namespacing\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: (sharedSignal?: AbortSignal) => Promise<T>,\n userKey: string,\n options?: { ttl?: number; callerSignal?: AbortSignal },\n ): Promise<T> {\n if (!this.config.enabled) return fn(options?.callerSignal);\n\n const callerSignal = options?.callerSignal;\n if (callerSignal?.aborted) {\n throw createAbortError(callerSignal);\n }\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 — join\n // the existing in-flight entry under reference counting so this\n // caller's abort doesn't poison the shared result.\n const existing = this.inFlightRequests.get(cacheKey) as\n | InFlightEntry<T>\n | undefined;\n if (existing && !existing.sharedController.signal.aborted) {\n existing.refCount++;\n // Cancel any pending abort timer — a new caller has joined\n if (existing.abortTimer) {\n clearTimeout(existing.abortTimer);\n existing.abortTimer = undefined;\n }\n span.setAttribute(\"cache.hit\", true);\n span.setAttribute(\"cache.deduplication\", true);\n span.addEvent(\"cache.deduplication_used\", {\n \"cache.key\": cacheKey,\n });\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 return await this._waitWithRefCount(existing, callerSignal);\n }\n\n // cache miss - execute function under a shared abort controller\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 sharedController = new AbortController();\n const entry: InFlightEntry<T> = {\n promise: undefined as unknown as Promise<T>,\n refCount: 1,\n sharedController,\n };\n\n entry.promise = fn(sharedController.signal)\n .then(async (result) => {\n await this.set(cacheKey, result, options);\n span.addEvent(\"cache.value_stored\", {\n \"cache.key\": cacheKey,\n \"cache.ttl\": options?.ttl ?? this.config.ttl ?? 3600,\n });\n return result;\n })\n .catch((error) => {\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n // If the shared controller aborted, all callers have already\n // abandoned the request (or are about to via their own signals)\n // — propagate the original error without wrapping. No live\n // awaiter will observe this rejection.\n if (sharedController.signal.aborted) {\n throw error;\n }\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 if (this.inFlightRequests.get(cacheKey) === entry) {\n this.inFlightRequests.delete(cacheKey);\n }\n });\n\n // Suppress unhandled rejection warnings when every caller bailed\n // before fn() resolved (their own promises rejected via\n // waitWithRefCount; the underlying entry.promise has no awaiter).\n entry.promise.catch(() => {});\n\n this.inFlightRequests.set(cacheKey, entry as InFlightEntry<unknown>);\n\n const result = await this._waitWithRefCount(entry, callerSignal);\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 * Wait on an in-flight entry, racing the underlying promise against the\n * caller's abort signal. When the caller aborts, the entry's refCount is\n * decremented; if it hits zero the shared controller is aborted so the\n * underlying `fn()` can stop. Other callers continue to await the same\n * entry and receive the result when it arrives.\n */\n private _waitWithRefCount<T>(\n entry: InFlightEntry<T>,\n callerSignal?: AbortSignal,\n ): Promise<T> {\n if (!callerSignal) return entry.promise;\n\n return new Promise<T>((resolve, reject) => {\n let settled = false;\n\n const release = () => {\n if (entry.refCount > 0) entry.refCount--;\n if (entry.refCount <= 0 && !entry.sharedController.signal.aborted) {\n // Grace period: delay abort so a StrictMode remount can join\n // the in-flight entry before the shared execution is cancelled.\n entry.abortTimer = setTimeout(() => {\n if (entry.refCount <= 0 && !entry.sharedController.signal.aborted) {\n entry.sharedController.abort(\n callerSignal.reason ?? \"all cache callers aborted\",\n );\n }\n }, CacheManager.ABORT_GRACE_PERIOD_MS);\n }\n };\n\n const onAbort = () => {\n if (settled) return;\n settled = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n release();\n reject(createAbortError(callerSignal));\n };\n\n if (callerSignal.aborted) {\n onAbort();\n return;\n }\n\n callerSignal.addEventListener(\"abort\", onAbort, { once: true });\n\n entry.promise.then(\n (value) => {\n if (settled) return;\n settled = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n resolve(value);\n },\n (error) => {\n if (settled) return;\n settled = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n reject(error);\n },\n );\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 for (const entry of this.inFlightRequests.values()) {\n if (entry.abortTimer) clearTimeout(entry.abortTimer);\n }\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\";\n\nimport type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\n\nimport { createLakebasePool } from \"../connectors/lakebase\";\nimport { getClientOptions } from \"../context/client-options\";\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 { ApiError, createWorkspaceClient } from \"../workspace-client\";\nimport { cacheDefaults } from \"./defaults\";\nimport { InMemoryStorage, PersistentStorage } from \"./storage\";\n\nconst logger = createLogger(\"cache\");\n\n/**\n * Reference-counted in-flight cache execution entry.\n *\n * `sharedController` decouples the cached `fn()` from any single caller's\n * abort signal. Callers join an in-flight entry by incrementing `refCount`;\n * when a caller aborts, refCount is decremented. The shared controller is\n * aborted only when refCount drops to 0 — i.e. all callers have abandoned\n * the request. This prevents one caller's cancellation (e.g. React\n * StrictMode unmount) from poisoning the in-flight result for other still-\n * connected awaiters.\n */\ninterface InFlightEntry<T> {\n promise: Promise<T>;\n refCount: number;\n sharedController: AbortController;\n abortTimer?: ReturnType<typeof setTimeout>;\n}\n\nfunction createAbortError(signal: AbortSignal): unknown {\n if (signal.reason !== undefined) return signal.reason;\n return new DOMException(\"The operation was aborted.\", \"AbortError\");\n}\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 static readonly ABORT_GRACE_PERIOD_MS = 100;\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, InFlightEntry<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 = createWorkspaceClient({\n clientOptions: getClientOptions(),\n }).toLegacyWorkspaceClient();\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 *\n * Multiple concurrent callers with the same `cacheKey` are deduplicated\n * onto a single in-flight execution. Each caller may pass its own\n * `callerSignal`; the underlying `fn()` is run with a shared, internally\n * managed `AbortSignal` that aborts only when *all* callers have\n * abandoned the request (reference counted). This decouples a single\n * caller's cancellation (e.g. React StrictMode unmount) from the shared\n * result, so other still-connected callers receive the cached value\n * normally.\n *\n * @param key - Cache key parts\n * @param fn - Function to execute. Receives the cache-owned shared signal;\n * pass it through to the underlying I/O so the work is cancelled when\n * no caller is left waiting.\n * @param userKey - User key for cache namespacing\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: (sharedSignal?: AbortSignal) => Promise<T>,\n userKey: string,\n options?: { ttl?: number; callerSignal?: AbortSignal },\n ): Promise<T> {\n if (!this.config.enabled) return fn(options?.callerSignal);\n\n const callerSignal = options?.callerSignal;\n if (callerSignal?.aborted) {\n throw createAbortError(callerSignal);\n }\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 — join\n // the existing in-flight entry under reference counting so this\n // caller's abort doesn't poison the shared result.\n const existing = this.inFlightRequests.get(cacheKey) as\n | InFlightEntry<T>\n | undefined;\n if (existing && !existing.sharedController.signal.aborted) {\n existing.refCount++;\n // Cancel any pending abort timer — a new caller has joined\n if (existing.abortTimer) {\n clearTimeout(existing.abortTimer);\n existing.abortTimer = undefined;\n }\n span.setAttribute(\"cache.hit\", true);\n span.setAttribute(\"cache.deduplication\", true);\n span.addEvent(\"cache.deduplication_used\", {\n \"cache.key\": cacheKey,\n });\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 return await this._waitWithRefCount(existing, callerSignal);\n }\n\n // cache miss - execute function under a shared abort controller\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 sharedController = new AbortController();\n const entry: InFlightEntry<T> = {\n promise: undefined as unknown as Promise<T>,\n refCount: 1,\n sharedController,\n };\n\n entry.promise = fn(sharedController.signal)\n .then(async (result) => {\n await this.set(cacheKey, result, options);\n span.addEvent(\"cache.value_stored\", {\n \"cache.key\": cacheKey,\n \"cache.ttl\": options?.ttl ?? this.config.ttl ?? 3600,\n });\n return result;\n })\n .catch((error) => {\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n // If the shared controller aborted, all callers have already\n // abandoned the request (or are about to via their own signals)\n // — propagate the original error without wrapping. No live\n // awaiter will observe this rejection.\n if (sharedController.signal.aborted) {\n throw error;\n }\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 if (this.inFlightRequests.get(cacheKey) === entry) {\n this.inFlightRequests.delete(cacheKey);\n }\n });\n\n // Suppress unhandled rejection warnings when every caller bailed\n // before fn() resolved (their own promises rejected via\n // waitWithRefCount; the underlying entry.promise has no awaiter).\n entry.promise.catch(() => {});\n\n this.inFlightRequests.set(cacheKey, entry as InFlightEntry<unknown>);\n\n const result = await this._waitWithRefCount(entry, callerSignal);\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 * Wait on an in-flight entry, racing the underlying promise against the\n * caller's abort signal. When the caller aborts, the entry's refCount is\n * decremented; if it hits zero the shared controller is aborted so the\n * underlying `fn()` can stop. Other callers continue to await the same\n * entry and receive the result when it arrives.\n */\n private _waitWithRefCount<T>(\n entry: InFlightEntry<T>,\n callerSignal?: AbortSignal,\n ): Promise<T> {\n if (!callerSignal) return entry.promise;\n\n return new Promise<T>((resolve, reject) => {\n let settled = false;\n\n const release = () => {\n if (entry.refCount > 0) entry.refCount--;\n if (entry.refCount <= 0 && !entry.sharedController.signal.aborted) {\n // Grace period: delay abort so a StrictMode remount can join\n // the in-flight entry before the shared execution is cancelled.\n entry.abortTimer = setTimeout(() => {\n if (entry.refCount <= 0 && !entry.sharedController.signal.aborted) {\n entry.sharedController.abort(\n callerSignal.reason ?? \"all cache callers aborted\",\n );\n }\n }, CacheManager.ABORT_GRACE_PERIOD_MS);\n }\n };\n\n const onAbort = () => {\n if (settled) return;\n settled = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n release();\n reject(createAbortError(callerSignal));\n };\n\n if (callerSignal.aborted) {\n onAbort();\n return;\n }\n\n callerSignal.addEventListener(\"abort\", onAbort, { once: true });\n\n entry.promise.then(\n (value) => {\n if (settled) return;\n settled = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n resolve(value);\n },\n (error) => {\n if (settled) return;\n settled = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n reject(error);\n },\n );\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 for (const entry of this.inFlightRequests.values()) {\n if (entry.abortTimer) clearTimeout(entry.abortTimer);\n }\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 +1 @@
1
- {"version":3,"file":"memory.js","names":[],"sources":["../../../src/cache/storage/memory.ts"],"sourcesContent":["import type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\nimport { inMemoryStorageDefaults } from \"./defaults\";\n\n/**\n * In-memory cache storage implementation. Uses a least recently used (LRU) eviction policy\n * to manage memory usage and ensure efficient cache operations.\n */\nexport class InMemoryStorage implements CacheStorage {\n private cache: Map<string, CacheEntry> = new Map();\n private accessOrder: Map<string, number> = new Map();\n private accessCounter: number;\n private maxSize: number;\n\n constructor(config: CacheConfig) {\n this.cache = new Map();\n this.accessOrder = new Map();\n this.maxSize = config.maxSize ?? inMemoryStorageDefaults.maxSize;\n this.accessCounter = 0;\n }\n\n /** Get an entry from the cache */\n async get<T>(key: string): Promise<CacheEntry<T> | null> {\n const entry = this.cache.get(key);\n if (!entry) return null;\n\n this.accessOrder.set(key, ++this.accessCounter);\n return entry as CacheEntry<T>;\n }\n\n /** Set an entry in the cache */\n async set<T>(key: string, entry: CacheEntry<T>): Promise<void> {\n if (this.cache.size >= this.maxSize && !this.cache.has(key)) {\n this.evictLRU();\n }\n\n this.cache.set(key, entry);\n this.accessOrder.set(key, ++this.accessCounter);\n }\n\n /** Delete an entry from the cache */\n async delete(key: string): Promise<void> {\n this.cache.delete(key);\n this.accessOrder.delete(key);\n }\n\n /** Clean in-memory cache */\n async clear(): Promise<void> {\n this.cache.clear();\n this.accessOrder.clear();\n this.accessCounter = 0;\n }\n\n /** Check if the cache has an entry */\n async has(key: string): Promise<boolean> {\n const entry = this.cache.get(key);\n if (!entry) return false;\n\n if (Date.now() > entry.expiry) {\n this.cache.delete(key);\n this.accessOrder.delete(key);\n return false;\n }\n\n return true;\n }\n\n /** Get the size of the cache */\n async size(): Promise<number> {\n return this.cache.size;\n }\n\n /** Check if the cache is persistent */\n isPersistent(): boolean {\n return false;\n }\n\n /** Check the health of the cache */\n async healthCheck(): Promise<boolean> {\n return true;\n }\n\n /** Close the cache */\n async close(): Promise<void> {\n this.cache.clear();\n this.accessOrder.clear();\n this.accessCounter = 0;\n }\n\n /** Evict the least recently used entry (LRU) */\n private evictLRU(): void {\n let oldestKey: string | null = null;\n let oldestAccess = Infinity;\n\n for (const [key, accessTime] of this.accessOrder) {\n if (accessTime < oldestAccess) {\n oldestAccess = accessTime;\n oldestKey = key;\n }\n }\n\n if (oldestKey) {\n this.cache.delete(oldestKey);\n this.accessOrder.delete(oldestKey);\n }\n }\n}\n"],"mappings":";;;;;;;AAOA,IAAa,kBAAb,MAAqD;CACnD,AAAQ,wBAAiC,IAAI,KAAK;CAClD,AAAQ,8BAAmC,IAAI,KAAK;CACpD,AAAQ;CACR,AAAQ;CAER,YAAY,QAAqB;AAC/B,OAAK,wBAAQ,IAAI,KAAK;AACtB,OAAK,8BAAc,IAAI,KAAK;AAC5B,OAAK,UAAU,OAAO,WAAW,wBAAwB;AACzD,OAAK,gBAAgB;;;CAIvB,MAAM,IAAO,KAA4C;EACvD,MAAM,QAAQ,KAAK,MAAM,IAAI,IAAI;AACjC,MAAI,CAAC,MAAO,QAAO;AAEnB,OAAK,YAAY,IAAI,KAAK,EAAE,KAAK,cAAc;AAC/C,SAAO;;;CAIT,MAAM,IAAO,KAAa,OAAqC;AAC7D,MAAI,KAAK,MAAM,QAAQ,KAAK,WAAW,CAAC,KAAK,MAAM,IAAI,IAAI,CACzD,MAAK,UAAU;AAGjB,OAAK,MAAM,IAAI,KAAK,MAAM;AAC1B,OAAK,YAAY,IAAI,KAAK,EAAE,KAAK,cAAc;;;CAIjD,MAAM,OAAO,KAA4B;AACvC,OAAK,MAAM,OAAO,IAAI;AACtB,OAAK,YAAY,OAAO,IAAI;;;CAI9B,MAAM,QAAuB;AAC3B,OAAK,MAAM,OAAO;AAClB,OAAK,YAAY,OAAO;AACxB,OAAK,gBAAgB;;;CAIvB,MAAM,IAAI,KAA+B;EACvC,MAAM,QAAQ,KAAK,MAAM,IAAI,IAAI;AACjC,MAAI,CAAC,MAAO,QAAO;AAEnB,MAAI,KAAK,KAAK,GAAG,MAAM,QAAQ;AAC7B,QAAK,MAAM,OAAO,IAAI;AACtB,QAAK,YAAY,OAAO,IAAI;AAC5B,UAAO;;AAGT,SAAO;;;CAIT,MAAM,OAAwB;AAC5B,SAAO,KAAK,MAAM;;;CAIpB,eAAwB;AACtB,SAAO;;;CAIT,MAAM,cAAgC;AACpC,SAAO;;;CAIT,MAAM,QAAuB;AAC3B,OAAK,MAAM,OAAO;AAClB,OAAK,YAAY,OAAO;AACxB,OAAK,gBAAgB;;;CAIvB,AAAQ,WAAiB;EACvB,IAAI,YAA2B;EAC/B,IAAI,eAAe;AAEnB,OAAK,MAAM,CAAC,KAAK,eAAe,KAAK,YACnC,KAAI,aAAa,cAAc;AAC7B,kBAAe;AACf,eAAY;;AAIhB,MAAI,WAAW;AACb,QAAK,MAAM,OAAO,UAAU;AAC5B,QAAK,YAAY,OAAO,UAAU"}
1
+ {"version":3,"file":"memory.js","names":[],"sources":["../../../src/cache/storage/memory.ts"],"sourcesContent":["import type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\n\nimport { inMemoryStorageDefaults } from \"./defaults\";\n\n/**\n * In-memory cache storage implementation. Uses a least recently used (LRU) eviction policy\n * to manage memory usage and ensure efficient cache operations.\n */\nexport class InMemoryStorage implements CacheStorage {\n private cache: Map<string, CacheEntry> = new Map();\n private accessOrder: Map<string, number> = new Map();\n private accessCounter: number;\n private maxSize: number;\n\n constructor(config: CacheConfig) {\n this.cache = new Map();\n this.accessOrder = new Map();\n this.maxSize = config.maxSize ?? inMemoryStorageDefaults.maxSize;\n this.accessCounter = 0;\n }\n\n /** Get an entry from the cache */\n async get<T>(key: string): Promise<CacheEntry<T> | null> {\n const entry = this.cache.get(key);\n if (!entry) return null;\n\n this.accessOrder.set(key, ++this.accessCounter);\n return entry as CacheEntry<T>;\n }\n\n /** Set an entry in the cache */\n async set<T>(key: string, entry: CacheEntry<T>): Promise<void> {\n if (this.cache.size >= this.maxSize && !this.cache.has(key)) {\n this.evictLRU();\n }\n\n this.cache.set(key, entry);\n this.accessOrder.set(key, ++this.accessCounter);\n }\n\n /** Delete an entry from the cache */\n async delete(key: string): Promise<void> {\n this.cache.delete(key);\n this.accessOrder.delete(key);\n }\n\n /** Clean in-memory cache */\n async clear(): Promise<void> {\n this.cache.clear();\n this.accessOrder.clear();\n this.accessCounter = 0;\n }\n\n /** Check if the cache has an entry */\n async has(key: string): Promise<boolean> {\n const entry = this.cache.get(key);\n if (!entry) return false;\n\n if (Date.now() > entry.expiry) {\n this.cache.delete(key);\n this.accessOrder.delete(key);\n return false;\n }\n\n return true;\n }\n\n /** Get the size of the cache */\n async size(): Promise<number> {\n return this.cache.size;\n }\n\n /** Check if the cache is persistent */\n isPersistent(): boolean {\n return false;\n }\n\n /** Check the health of the cache */\n async healthCheck(): Promise<boolean> {\n return true;\n }\n\n /** Close the cache */\n async close(): Promise<void> {\n this.cache.clear();\n this.accessOrder.clear();\n this.accessCounter = 0;\n }\n\n /** Evict the least recently used entry (LRU) */\n private evictLRU(): void {\n let oldestKey: string | null = null;\n let oldestAccess = Infinity;\n\n for (const [key, accessTime] of this.accessOrder) {\n if (accessTime < oldestAccess) {\n oldestAccess = accessTime;\n oldestKey = key;\n }\n }\n\n if (oldestKey) {\n this.cache.delete(oldestKey);\n this.accessOrder.delete(oldestKey);\n }\n }\n}\n"],"mappings":";;;;;;;AAQA,IAAa,kBAAb,MAAqD;CACnD,AAAQ,wBAAiC,IAAI,KAAK;CAClD,AAAQ,8BAAmC,IAAI,KAAK;CACpD,AAAQ;CACR,AAAQ;CAER,YAAY,QAAqB;AAC/B,OAAK,wBAAQ,IAAI,KAAK;AACtB,OAAK,8BAAc,IAAI,KAAK;AAC5B,OAAK,UAAU,OAAO,WAAW,wBAAwB;AACzD,OAAK,gBAAgB;;;CAIvB,MAAM,IAAO,KAA4C;EACvD,MAAM,QAAQ,KAAK,MAAM,IAAI,IAAI;AACjC,MAAI,CAAC,MAAO,QAAO;AAEnB,OAAK,YAAY,IAAI,KAAK,EAAE,KAAK,cAAc;AAC/C,SAAO;;;CAIT,MAAM,IAAO,KAAa,OAAqC;AAC7D,MAAI,KAAK,MAAM,QAAQ,KAAK,WAAW,CAAC,KAAK,MAAM,IAAI,IAAI,CACzD,MAAK,UAAU;AAGjB,OAAK,MAAM,IAAI,KAAK,MAAM;AAC1B,OAAK,YAAY,IAAI,KAAK,EAAE,KAAK,cAAc;;;CAIjD,MAAM,OAAO,KAA4B;AACvC,OAAK,MAAM,OAAO,IAAI;AACtB,OAAK,YAAY,OAAO,IAAI;;;CAI9B,MAAM,QAAuB;AAC3B,OAAK,MAAM,OAAO;AAClB,OAAK,YAAY,OAAO;AACxB,OAAK,gBAAgB;;;CAIvB,MAAM,IAAI,KAA+B;EACvC,MAAM,QAAQ,KAAK,MAAM,IAAI,IAAI;AACjC,MAAI,CAAC,MAAO,QAAO;AAEnB,MAAI,KAAK,KAAK,GAAG,MAAM,QAAQ;AAC7B,QAAK,MAAM,OAAO,IAAI;AACtB,QAAK,YAAY,OAAO,IAAI;AAC5B,UAAO;;AAGT,SAAO;;;CAIT,MAAM,OAAwB;AAC5B,SAAO,KAAK,MAAM;;;CAIpB,eAAwB;AACtB,SAAO;;;CAIT,MAAM,cAAgC;AACpC,SAAO;;;CAIT,MAAM,QAAuB;AAC3B,OAAK,MAAM,OAAO;AAClB,OAAK,YAAY,OAAO;AACxB,OAAK,gBAAgB;;;CAIvB,AAAQ,WAAiB;EACvB,IAAI,YAA2B;EAC/B,IAAI,eAAe;AAEnB,OAAK,MAAM,CAAC,KAAK,eAAe,KAAK,YACnC,KAAI,aAAa,cAAc;AAC7B,kBAAe;AACf,eAAY;;AAIhB,MAAI,WAAW;AACb,QAAK,MAAM,OAAO,UAAU;AAC5B,QAAK,YAAY,OAAO,UAAU"}
@@ -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\";\n\nimport type pg from \"pg\";\nimport type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\n\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":"index.js","names":[],"sources":["../../../../src/cli/commands/codemod/index.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { onPluginsReadyCommand } from \"./on-plugins-ready\";\n\n/**\n * Parent command for codemod operations.\n * Subcommands:\n * - on-plugins-ready: Migrate from autoStart/extend/start to onPluginsReady callback\n */\nexport const codemodCommand = new Command(\"codemod\")\n .description(\"Run codemods to migrate to newer AppKit APIs\")\n .addCommand(onPluginsReadyCommand)\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit codemod on-plugins-ready --write`,\n );\n"],"mappings":";;;;;;;;;AAQA,MAAa,iBAAiB,IAAI,QAAQ,UAAU,CACjD,YAAY,+CAA+C,CAC3D,WAAW,sBAAsB,CACjC,YACC,SACA;;6CAGD"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../src/cli/commands/codemod/index.ts"],"sourcesContent":["import { Command } from \"commander\";\n\nimport { onPluginsReadyCommand } from \"./on-plugins-ready\";\n\n/**\n * Parent command for codemod operations.\n * Subcommands:\n * - on-plugins-ready: Migrate from autoStart/extend/start to onPluginsReady callback\n */\nexport const codemodCommand = new Command(\"codemod\")\n .description(\"Run codemods to migrate to newer AppKit APIs\")\n .addCommand(onPluginsReadyCommand)\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit codemod on-plugins-ready --write`,\n );\n"],"mappings":";;;;;;;;;AASA,MAAa,iBAAiB,IAAI,QAAQ,UAAU,CACjD,YAAY,+CAA+C,CAC3D,WAAW,sBAAsB,CACjC,YACC,SACA;;6CAGD"}
@@ -1 +1 @@
1
- {"version":3,"file":"on-plugins-ready.js","names":[],"sources":["../../../../src/cli/commands/codemod/on-plugins-ready.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Lang, parse } from \"@ast-grep/napi\";\nimport { Command } from \"commander\";\n\nconst SEARCH_DIRS = [\"server\", \"src\", \".\"];\nconst CANDIDATE_NAMES = [\"server.ts\", \"index.ts\"];\nconst SKIP_DIRS = new Set([\"node_modules\", \"dist\", \"build\", \".git\"]);\n\nfunction findServerEntryFiles(rootDir: string): string[] {\n const results: string[] = [];\n\n for (const dir of SEARCH_DIRS) {\n const absDir = path.resolve(rootDir, dir);\n if (!fs.existsSync(absDir)) continue;\n\n const files =\n dir === \".\"\n ? CANDIDATE_NAMES.map((n) => path.join(absDir, n)).filter(fs.existsSync)\n : findTsFiles(absDir);\n\n for (const file of files) {\n const content = fs.readFileSync(file, \"utf-8\");\n if (\n content.includes(\"createApp\") &&\n content.includes(\"@databricks/appkit\")\n ) {\n results.push(file);\n }\n }\n }\n\n return [...new Set(results)];\n}\n\nfunction findTsFiles(dir: string, files: string[] = []): string[] {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n return files;\n }\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name)) continue;\n findTsFiles(fullPath, files);\n } else if (entry.isFile() && entry.name.endsWith(\".ts\")) {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\nfunction isAlreadyMigrated(content: string): boolean {\n const ast = parse(Lang.TypeScript, content);\n const root = ast.root();\n return root.findAll(\"createApp({ $$$PROPS })\").some((match) => {\n const text = match.text();\n return /\\bonPluginsReady\\s*[(:]/.test(text);\n });\n}\n\n/**\n * Find the index of the matching closing delimiter for an opening one.\n * Supports (), {}, and [].\n */\nfunction findMatchingClose(content: string, openIdx: number): number {\n const open = content[openIdx];\n const closeMap: Record<string, string> = {\n \"(\": \")\",\n \"{\": \"}\",\n \"[\": \"]\",\n };\n const close = closeMap[open];\n if (!close) return -1;\n\n let depth = 1;\n let i = openIdx + 1;\n while (i < content.length && depth > 0) {\n const ch = content[i];\n if (ch === open) depth++;\n else if (ch === close) depth--;\n\n // skip string literals\n if (ch === '\"' || ch === \"'\" || ch === \"`\") {\n i = skipString(content, i);\n continue;\n }\n i++;\n }\n return depth === 0 ? i - 1 : -1;\n}\n\nfunction skipString(content: string, startIdx: number): number {\n const quote = content[startIdx];\n let i = startIdx + 1;\n while (i < content.length) {\n if (content[i] === \"\\\\\") {\n i += 2;\n continue;\n }\n if (content[i] === quote) return i + 1;\n i++;\n }\n return i;\n}\n\nfunction stripAutoStartFromServerCalls(content: string): string {\n return content.replace(\n /server\\(\\{([^}]*)\\}\\)/g,\n (_fullMatch, propsStr: string) => {\n const cleaned = propsStr\n .replace(/autoStart\\s*:\\s*(true|false)\\s*,?\\s*/g, \"\")\n .replace(/,\\s*$/, \"\")\n .trim();\n if (!cleaned) return \"server()\";\n return `server({ ${cleaned} })`;\n },\n );\n}\n\ninterface MigrationResult {\n migrated: boolean;\n content: string;\n warnings: string[];\n}\n\nfunction migratePatternA(content: string): MigrationResult {\n const warnings: string[] = [];\n\n // Find createApp(...).then(\n const createAppIdx = content.indexOf(\"createApp(\");\n if (createAppIdx === -1) return { migrated: false, content, warnings };\n\n // Find the opening paren of createApp(\n const configOpenParen = content.indexOf(\"(\", createAppIdx);\n const configCloseParen = findMatchingClose(content, configOpenParen);\n if (configCloseParen === -1) return { migrated: false, content, warnings };\n\n // Check for .then( after the closing paren\n const afterCreateApp = content.slice(configCloseParen + 1);\n const thenMatch = afterCreateApp.match(/^\\s*\\.then\\s*\\(/);\n if (!thenMatch) return { migrated: false, content, warnings };\n\n const thenStart = configCloseParen + 1 + afterCreateApp.indexOf(\".then\");\n const thenOpenParen = content.indexOf(\"(\", thenStart + 4);\n const thenCloseParen = findMatchingClose(content, thenOpenParen);\n if (thenCloseParen === -1) return { migrated: false, content, warnings };\n\n // Extract the callback inside .then(...)\n const thenRaw = content.slice(thenOpenParen + 1, thenCloseParen);\n const thenInner = thenRaw.trim();\n\n // Parse callback: (param) => { body } or async (param) => { body }\n const callbackMatch = thenInner.match(\n /^(?:async\\s+)?\\(\\s*(\\w+)\\s*\\)\\s*=>\\s*\\{/,\n );\n if (!callbackMatch) return { migrated: false, content, warnings };\n\n const paramName = callbackMatch[1];\n const bodyOpenBrace = thenOpenParen + 1 + thenRaw.indexOf(\"{\");\n const bodyCloseBrace = findMatchingClose(content, bodyOpenBrace);\n if (bodyCloseBrace === -1) return { migrated: false, content, warnings };\n\n let callbackBody = content.slice(bodyOpenBrace + 1, bodyCloseBrace).trim();\n\n // Remove entire statements that are just .start() calls (e.g. `await appkit.server.start();`)\n callbackBody = callbackBody\n .replace(/^\\s*(?:await\\s+)?\\w+\\.server\\s*\\.\\s*start\\(\\s*\\)\\s*;?\\s*$/gm, \"\")\n .replace(/\\n\\s*\\.start\\(\\s*\\)\\s*;?/g, \";\")\n .replace(/\\.start\\(\\s*\\)/g, \"\")\n .replace(/\\n\\s*\\n\\s*\\n/g, \"\\n\\n\")\n .trim();\n\n // Clean up trailing semicolons\n if (callbackBody.endsWith(\";\")) {\n // fine\n } else if (!callbackBody.endsWith(\"}\")) {\n callbackBody += \";\";\n }\n\n // Detect if the callback was async\n const isAsync = /^async\\s/.test(thenInner.trim());\n\n // Check for .catch() after .then(...) using brace-aware parsing\n const afterThenClose = content.slice(thenCloseParen + 1);\n const catchPatternMatch = afterThenClose.match(/^\\s*(?:\\)\\s*)?\\.catch\\s*\\(/);\n\n let catchSuffix: string;\n let consumeAfterThen: number;\n\n if (catchPatternMatch) {\n const catchOpenParen = thenCloseParen + 1 + catchPatternMatch[0].length - 1;\n const catchCloseParen = findMatchingClose(content, catchOpenParen);\n if (catchCloseParen !== -1) {\n const catchArg = content\n .slice(catchOpenParen + 1, catchCloseParen)\n .trim();\n catchSuffix = `.catch(${catchArg})`;\n consumeAfterThen = catchCloseParen + 1 - (thenCloseParen + 1);\n } else {\n catchSuffix = \".catch(console.error)\";\n consumeAfterThen = 0;\n }\n } else {\n catchSuffix = \".catch(console.error)\";\n consumeAfterThen = 0;\n }\n\n // Build the onPluginsReady property\n const configStr = content.slice(configOpenParen + 1, configCloseParen);\n const lastBraceIdx = configStr.lastIndexOf(\"}\");\n if (lastBraceIdx === -1) return { migrated: false, content, warnings };\n\n const beforeLastBrace = configStr.slice(0, lastBraceIdx).trimEnd();\n const needsComma = beforeLastBrace.endsWith(\",\") ? \"\" : \",\";\n\n // Indent the body properly\n const bodyLines = callbackBody.split(\"\\n\");\n const indentedBody = bodyLines\n .map((line) => ` ${line.trimStart()}`)\n .join(\"\\n\");\n\n const asyncPrefix = isAsync ? \"async \" : \"\";\n const onPluginsReadyProp = `${needsComma}\\n ${asyncPrefix}onPluginsReady(${paramName}) {\\n${indentedBody}\\n },`;\n const newConfig = `${beforeLastBrace}${onPluginsReadyProp}\\n}`;\n\n // Build the replacement\n const endIdx = thenCloseParen + 1 + consumeAfterThen;\n // Consume trailing ) ; and whitespace\n let finalEnd = endIdx;\n const trailing = content.slice(finalEnd).match(/^\\s*\\)?\\s*;?\\s*/);\n if (trailing) finalEnd += trailing[0].length;\n\n const newContent =\n content.slice(0, createAppIdx) +\n `createApp(${newConfig})${catchSuffix};` +\n content.slice(finalEnd);\n\n return { migrated: true, content: newContent, warnings };\n}\n\nfunction migratePatternB(content: string): MigrationResult {\n const warnings: string[] = [];\n\n // Match: const/let varName = await createApp({...});\n const awaitPattern = /(?:const|let)\\s+(\\w+)\\s*=\\s*await\\s+createApp\\s*\\(/;\n\n const match = content.match(awaitPattern);\n if (!match) return { migrated: false, content, warnings };\n\n const varName = match[1];\n const matchIdx = content.indexOf(match[0]);\n\n // Find the createApp(...) closing paren\n const configOpenParen = matchIdx + match[0].length - 1;\n const configCloseParen = findMatchingClose(content, configOpenParen);\n if (configCloseParen === -1) return { migrated: false, content, warnings };\n\n // Find the semicolon after the createApp call\n const afterCall = content.slice(configCloseParen + 1);\n const semiMatch = afterCall.match(/^\\s*;/);\n const createAppEnd =\n configCloseParen + 1 + (semiMatch ? semiMatch[0].length : 0);\n\n // Find all uses of varName after the createApp call\n const afterCreateApp = content.slice(createAppEnd);\n const varUsagePattern = new RegExp(`\\\\b${varName}\\\\.(\\\\w+)`, \"g\");\n\n const usages: { plugin: string; index: number }[] = [];\n for (const usageMatch of afterCreateApp.matchAll(varUsagePattern)) {\n usages.push({ plugin: usageMatch[1], index: usageMatch.index });\n }\n\n // Check for non-server usage\n const nonServerUsage = usages.filter((u) => u.plugin !== \"server\");\n if (nonServerUsage.length > 0) {\n warnings.push(\n `Found additional usage of '${varName}' handle outside server.extend/start. Please migrate manually.`,\n );\n return { migrated: false, content, warnings };\n }\n\n // Find the extend call(s) and start call in the after-createApp region\n const extendPattern = new RegExp(\n `\\\\b${varName}\\\\.server\\\\.extend\\\\s*\\\\(`,\n \"g\",\n );\n const startPattern = new RegExp(\n `(?:await\\\\s+)?${varName}\\\\.server\\\\.start\\\\s*\\\\(\\\\s*\\\\)\\\\s*;`,\n );\n\n const extendMatches = [...afterCreateApp.matchAll(extendPattern)];\n if (extendMatches.length > 1) {\n warnings.push(\n `Found ${extendMatches.length} server.extend() calls. Please migrate manually.`,\n );\n return { migrated: false, content, warnings };\n }\n\n const extendExec = extendMatches[0] ?? null;\n const startExec = startPattern.exec(afterCreateApp);\n\n if (!startExec) return { migrated: false, content, warnings };\n\n // Extract the extend call's argument\n let extendArg = \"\";\n let extendFullStatement = \"\";\n if (extendExec) {\n const extendOpenParen =\n createAppEnd + extendExec.index + extendExec[0].length - 1;\n const extendCloseParen = findMatchingClose(content, extendOpenParen);\n if (extendCloseParen !== -1) {\n extendArg = content.slice(extendOpenParen + 1, extendCloseParen).trim();\n // Find the full statement including trailing semicolon\n const stmtStart = createAppEnd + extendExec.index;\n let stmtEnd = extendCloseParen + 1;\n const afterExtend = content.slice(stmtEnd);\n const trailingSemi = afterExtend.match(/^\\s*;/);\n if (trailingSemi) stmtEnd += trailingSemi[0].length;\n extendFullStatement = content.slice(stmtStart, stmtEnd);\n }\n }\n\n const startFullStatement = startExec[0];\n\n // Build the onPluginsReady callback\n const configStr = content.slice(configOpenParen + 1, configCloseParen);\n const lastBraceIdx = configStr.lastIndexOf(\"}\");\n if (lastBraceIdx === -1) return { migrated: false, content, warnings };\n\n const beforeLastBrace = configStr.slice(0, lastBraceIdx).trimEnd();\n const needsComma = beforeLastBrace.endsWith(\",\") ? \"\" : \",\";\n\n let onPluginsReadyProp: string;\n if (extendArg) {\n onPluginsReadyProp =\n `${needsComma}\\n onPluginsReady(${varName}) {\\n` +\n ` ${varName}.server.extend(${extendArg});\\n` +\n \" },\";\n } else {\n onPluginsReadyProp = \"\";\n }\n\n const newConfig = `${beforeLastBrace}${onPluginsReadyProp}\\n}`;\n const newCreateApp = `await createApp(${newConfig});`;\n\n // Replace: remove const declaration, replace with plain await, remove extend + start\n let result = content.slice(0, matchIdx) + newCreateApp;\n let remaining = afterCreateApp;\n\n if (extendFullStatement) {\n remaining = remaining.replace(extendFullStatement, \"\");\n }\n remaining = remaining.replace(startFullStatement, \"\");\n\n // Clean up consecutive blank lines\n remaining = remaining.replace(/\\n\\s*\\n\\s*\\n/g, \"\\n\\n\");\n\n result += remaining;\n\n return { migrated: true, content: result, warnings };\n}\n\nexport function migrateFile(filePath: string): MigrationResult {\n const original = fs.readFileSync(filePath, \"utf-8\");\n\n if (isAlreadyMigrated(original)) {\n return {\n migrated: false,\n content: original,\n warnings: [\"Already migrated -- no changes needed.\"],\n };\n }\n\n const content = stripAutoStartFromServerCalls(original);\n const allWarnings: string[] = [];\n\n // Try Pattern A first\n const patternA = migratePatternA(content);\n if (patternA.migrated) {\n allWarnings.push(...patternA.warnings);\n return {\n migrated: true,\n content: patternA.content,\n warnings: allWarnings,\n };\n }\n allWarnings.push(...patternA.warnings);\n\n // Try Pattern B\n const patternB = migratePatternB(content);\n if (patternB.migrated) {\n allWarnings.push(...patternB.warnings);\n return {\n migrated: true,\n content: patternB.content,\n warnings: allWarnings,\n };\n }\n allWarnings.push(...patternB.warnings);\n\n // Check if autoStart was stripped (content changed but no pattern matched)\n if (content !== original) {\n return { migrated: true, content, warnings: allWarnings };\n }\n\n return { migrated: false, content: original, warnings: allWarnings };\n}\n\nfunction runCodemod(options: { path?: string; write?: boolean }) {\n const rootDir = process.cwd();\n const write = options.write ?? false;\n\n let files: string[];\n if (options.path) {\n const absPath = path.resolve(rootDir, options.path);\n if (!fs.existsSync(absPath)) {\n console.error(`File not found: ${absPath}`);\n process.exit(1);\n }\n files = [absPath];\n } else {\n files = findServerEntryFiles(rootDir);\n }\n\n if (files.length === 0) {\n console.log(\"No files found importing createApp from @databricks/appkit.\");\n console.log(\"Use --path to specify a file explicitly.\");\n process.exit(0);\n }\n\n let hasChanges = false;\n\n for (const file of files) {\n const relPath = path.relative(rootDir, file);\n const result = migrateFile(file);\n\n for (const warning of result.warnings) {\n console.log(` ${relPath}: ${warning}`);\n }\n\n if (!result.migrated) {\n if (result.warnings.length === 0) {\n console.log(` ${relPath}: No migration needed.`);\n }\n continue;\n }\n\n hasChanges = true;\n\n if (write) {\n fs.writeFileSync(file, result.content, \"utf-8\");\n console.log(` ${relPath}: Migrated successfully.`);\n } else {\n console.log(`\\n--- ${relPath} (dry run) ---`);\n console.log(result.content);\n console.log(\"---\");\n }\n }\n\n if (hasChanges && !write) {\n console.log(\"\\nDry run complete. Run with --write to apply changes.\");\n }\n}\n\nexport const onPluginsReadyCommand = new Command(\"on-plugins-ready\")\n .description(\n \"Migrate createApp usage from autoStart/extend/start pattern to onPluginsReady callback\",\n )\n .option(\"--path <file>\", \"Path to the server entry file to migrate\")\n .option(\"--write\", \"Apply changes (default: dry-run)\", false)\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit codemod on-plugins-ready # dry-run, auto-detect files\n $ appkit codemod on-plugins-ready --write # apply changes\n $ appkit codemod on-plugins-ready --path server.ts # migrate a specific file`,\n )\n .action(runCodemod);\n"],"mappings":"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"}
1
+ {"version":3,"file":"on-plugins-ready.js","names":[],"sources":["../../../../src/cli/commands/codemod/on-plugins-ready.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { Lang, parse } from \"@ast-grep/napi\";\nimport { Command } from \"commander\";\n\nconst SEARCH_DIRS = [\"server\", \"src\", \".\"];\nconst CANDIDATE_NAMES = [\"server.ts\", \"index.ts\"];\nconst SKIP_DIRS = new Set([\"node_modules\", \"dist\", \"build\", \".git\"]);\n\nfunction findServerEntryFiles(rootDir: string): string[] {\n const results: string[] = [];\n\n for (const dir of SEARCH_DIRS) {\n const absDir = path.resolve(rootDir, dir);\n if (!fs.existsSync(absDir)) continue;\n\n const files =\n dir === \".\"\n ? CANDIDATE_NAMES.map((n) => path.join(absDir, n)).filter(fs.existsSync)\n : findTsFiles(absDir);\n\n for (const file of files) {\n const content = fs.readFileSync(file, \"utf-8\");\n if (\n content.includes(\"createApp\") &&\n content.includes(\"@databricks/appkit\")\n ) {\n results.push(file);\n }\n }\n }\n\n return [...new Set(results)];\n}\n\nfunction findTsFiles(dir: string, files: string[] = []): string[] {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n return files;\n }\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name)) continue;\n findTsFiles(fullPath, files);\n } else if (entry.isFile() && entry.name.endsWith(\".ts\")) {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\nfunction isAlreadyMigrated(content: string): boolean {\n const ast = parse(Lang.TypeScript, content);\n const root = ast.root();\n return root.findAll(\"createApp({ $$$PROPS })\").some((match) => {\n const text = match.text();\n return /\\bonPluginsReady\\s*[(:]/.test(text);\n });\n}\n\n/**\n * Find the index of the matching closing delimiter for an opening one.\n * Supports (), {}, and [].\n */\nfunction findMatchingClose(content: string, openIdx: number): number {\n const open = content[openIdx];\n const closeMap: Record<string, string> = {\n \"(\": \")\",\n \"{\": \"}\",\n \"[\": \"]\",\n };\n const close = closeMap[open];\n if (!close) return -1;\n\n let depth = 1;\n let i = openIdx + 1;\n while (i < content.length && depth > 0) {\n const ch = content[i];\n if (ch === open) depth++;\n else if (ch === close) depth--;\n\n // skip string literals\n if (ch === '\"' || ch === \"'\" || ch === \"`\") {\n i = skipString(content, i);\n continue;\n }\n i++;\n }\n return depth === 0 ? i - 1 : -1;\n}\n\nfunction skipString(content: string, startIdx: number): number {\n const quote = content[startIdx];\n let i = startIdx + 1;\n while (i < content.length) {\n if (content[i] === \"\\\\\") {\n i += 2;\n continue;\n }\n if (content[i] === quote) return i + 1;\n i++;\n }\n return i;\n}\n\nfunction stripAutoStartFromServerCalls(content: string): string {\n return content.replace(\n /server\\(\\{([^}]*)\\}\\)/g,\n (_fullMatch, propsStr: string) => {\n const cleaned = propsStr\n .replace(/autoStart\\s*:\\s*(true|false)\\s*,?\\s*/g, \"\")\n .replace(/,\\s*$/, \"\")\n .trim();\n if (!cleaned) return \"server()\";\n return `server({ ${cleaned} })`;\n },\n );\n}\n\ninterface MigrationResult {\n migrated: boolean;\n content: string;\n warnings: string[];\n}\n\nfunction migratePatternA(content: string): MigrationResult {\n const warnings: string[] = [];\n\n // Find createApp(...).then(\n const createAppIdx = content.indexOf(\"createApp(\");\n if (createAppIdx === -1) return { migrated: false, content, warnings };\n\n // Find the opening paren of createApp(\n const configOpenParen = content.indexOf(\"(\", createAppIdx);\n const configCloseParen = findMatchingClose(content, configOpenParen);\n if (configCloseParen === -1) return { migrated: false, content, warnings };\n\n // Check for .then( after the closing paren\n const afterCreateApp = content.slice(configCloseParen + 1);\n const thenMatch = afterCreateApp.match(/^\\s*\\.then\\s*\\(/);\n if (!thenMatch) return { migrated: false, content, warnings };\n\n const thenStart = configCloseParen + 1 + afterCreateApp.indexOf(\".then\");\n const thenOpenParen = content.indexOf(\"(\", thenStart + 4);\n const thenCloseParen = findMatchingClose(content, thenOpenParen);\n if (thenCloseParen === -1) return { migrated: false, content, warnings };\n\n // Extract the callback inside .then(...)\n const thenRaw = content.slice(thenOpenParen + 1, thenCloseParen);\n const thenInner = thenRaw.trim();\n\n // Parse callback: (param) => { body } or async (param) => { body }\n const callbackMatch = thenInner.match(\n /^(?:async\\s+)?\\(\\s*(\\w+)\\s*\\)\\s*=>\\s*\\{/,\n );\n if (!callbackMatch) return { migrated: false, content, warnings };\n\n const paramName = callbackMatch[1];\n const bodyOpenBrace = thenOpenParen + 1 + thenRaw.indexOf(\"{\");\n const bodyCloseBrace = findMatchingClose(content, bodyOpenBrace);\n if (bodyCloseBrace === -1) return { migrated: false, content, warnings };\n\n let callbackBody = content.slice(bodyOpenBrace + 1, bodyCloseBrace).trim();\n\n // Remove entire statements that are just .start() calls (e.g. `await appkit.server.start();`)\n callbackBody = callbackBody\n .replace(/^\\s*(?:await\\s+)?\\w+\\.server\\s*\\.\\s*start\\(\\s*\\)\\s*;?\\s*$/gm, \"\")\n .replace(/\\n\\s*\\.start\\(\\s*\\)\\s*;?/g, \";\")\n .replace(/\\.start\\(\\s*\\)/g, \"\")\n .replace(/\\n\\s*\\n\\s*\\n/g, \"\\n\\n\")\n .trim();\n\n // Clean up trailing semicolons\n if (callbackBody.endsWith(\";\")) {\n // fine\n } else if (!callbackBody.endsWith(\"}\")) {\n callbackBody += \";\";\n }\n\n // Detect if the callback was async\n const isAsync = /^async\\s/.test(thenInner.trim());\n\n // Check for .catch() after .then(...) using brace-aware parsing\n const afterThenClose = content.slice(thenCloseParen + 1);\n const catchPatternMatch = afterThenClose.match(/^\\s*(?:\\)\\s*)?\\.catch\\s*\\(/);\n\n let catchSuffix: string;\n let consumeAfterThen: number;\n\n if (catchPatternMatch) {\n const catchOpenParen = thenCloseParen + 1 + catchPatternMatch[0].length - 1;\n const catchCloseParen = findMatchingClose(content, catchOpenParen);\n if (catchCloseParen !== -1) {\n const catchArg = content\n .slice(catchOpenParen + 1, catchCloseParen)\n .trim();\n catchSuffix = `.catch(${catchArg})`;\n consumeAfterThen = catchCloseParen + 1 - (thenCloseParen + 1);\n } else {\n catchSuffix = \".catch(console.error)\";\n consumeAfterThen = 0;\n }\n } else {\n catchSuffix = \".catch(console.error)\";\n consumeAfterThen = 0;\n }\n\n // Build the onPluginsReady property\n const configStr = content.slice(configOpenParen + 1, configCloseParen);\n const lastBraceIdx = configStr.lastIndexOf(\"}\");\n if (lastBraceIdx === -1) return { migrated: false, content, warnings };\n\n const beforeLastBrace = configStr.slice(0, lastBraceIdx).trimEnd();\n const needsComma = beforeLastBrace.endsWith(\",\") ? \"\" : \",\";\n\n // Indent the body properly\n const bodyLines = callbackBody.split(\"\\n\");\n const indentedBody = bodyLines\n .map((line) => ` ${line.trimStart()}`)\n .join(\"\\n\");\n\n const asyncPrefix = isAsync ? \"async \" : \"\";\n const onPluginsReadyProp = `${needsComma}\\n ${asyncPrefix}onPluginsReady(${paramName}) {\\n${indentedBody}\\n },`;\n const newConfig = `${beforeLastBrace}${onPluginsReadyProp}\\n}`;\n\n // Build the replacement\n const endIdx = thenCloseParen + 1 + consumeAfterThen;\n // Consume trailing ) ; and whitespace\n let finalEnd = endIdx;\n const trailing = content.slice(finalEnd).match(/^\\s*\\)?\\s*;?\\s*/);\n if (trailing) finalEnd += trailing[0].length;\n\n const newContent =\n content.slice(0, createAppIdx) +\n `createApp(${newConfig})${catchSuffix};` +\n content.slice(finalEnd);\n\n return { migrated: true, content: newContent, warnings };\n}\n\nfunction migratePatternB(content: string): MigrationResult {\n const warnings: string[] = [];\n\n // Match: const/let varName = await createApp({...});\n const awaitPattern = /(?:const|let)\\s+(\\w+)\\s*=\\s*await\\s+createApp\\s*\\(/;\n\n const match = content.match(awaitPattern);\n if (!match) return { migrated: false, content, warnings };\n\n const varName = match[1];\n const matchIdx = content.indexOf(match[0]);\n\n // Find the createApp(...) closing paren\n const configOpenParen = matchIdx + match[0].length - 1;\n const configCloseParen = findMatchingClose(content, configOpenParen);\n if (configCloseParen === -1) return { migrated: false, content, warnings };\n\n // Find the semicolon after the createApp call\n const afterCall = content.slice(configCloseParen + 1);\n const semiMatch = afterCall.match(/^\\s*;/);\n const createAppEnd =\n configCloseParen + 1 + (semiMatch ? semiMatch[0].length : 0);\n\n // Find all uses of varName after the createApp call\n const afterCreateApp = content.slice(createAppEnd);\n const varUsagePattern = new RegExp(`\\\\b${varName}\\\\.(\\\\w+)`, \"g\");\n\n const usages: { plugin: string; index: number }[] = [];\n for (const usageMatch of afterCreateApp.matchAll(varUsagePattern)) {\n usages.push({ plugin: usageMatch[1], index: usageMatch.index });\n }\n\n // Check for non-server usage\n const nonServerUsage = usages.filter((u) => u.plugin !== \"server\");\n if (nonServerUsage.length > 0) {\n warnings.push(\n `Found additional usage of '${varName}' handle outside server.extend/start. Please migrate manually.`,\n );\n return { migrated: false, content, warnings };\n }\n\n // Find the extend call(s) and start call in the after-createApp region\n const extendPattern = new RegExp(\n `\\\\b${varName}\\\\.server\\\\.extend\\\\s*\\\\(`,\n \"g\",\n );\n const startPattern = new RegExp(\n `(?:await\\\\s+)?${varName}\\\\.server\\\\.start\\\\s*\\\\(\\\\s*\\\\)\\\\s*;`,\n );\n\n const extendMatches = [...afterCreateApp.matchAll(extendPattern)];\n if (extendMatches.length > 1) {\n warnings.push(\n `Found ${extendMatches.length} server.extend() calls. Please migrate manually.`,\n );\n return { migrated: false, content, warnings };\n }\n\n const extendExec = extendMatches[0] ?? null;\n const startExec = startPattern.exec(afterCreateApp);\n\n if (!startExec) return { migrated: false, content, warnings };\n\n // Extract the extend call's argument\n let extendArg = \"\";\n let extendFullStatement = \"\";\n if (extendExec) {\n const extendOpenParen =\n createAppEnd + extendExec.index + extendExec[0].length - 1;\n const extendCloseParen = findMatchingClose(content, extendOpenParen);\n if (extendCloseParen !== -1) {\n extendArg = content.slice(extendOpenParen + 1, extendCloseParen).trim();\n // Find the full statement including trailing semicolon\n const stmtStart = createAppEnd + extendExec.index;\n let stmtEnd = extendCloseParen + 1;\n const afterExtend = content.slice(stmtEnd);\n const trailingSemi = afterExtend.match(/^\\s*;/);\n if (trailingSemi) stmtEnd += trailingSemi[0].length;\n extendFullStatement = content.slice(stmtStart, stmtEnd);\n }\n }\n\n const startFullStatement = startExec[0];\n\n // Build the onPluginsReady callback\n const configStr = content.slice(configOpenParen + 1, configCloseParen);\n const lastBraceIdx = configStr.lastIndexOf(\"}\");\n if (lastBraceIdx === -1) return { migrated: false, content, warnings };\n\n const beforeLastBrace = configStr.slice(0, lastBraceIdx).trimEnd();\n const needsComma = beforeLastBrace.endsWith(\",\") ? \"\" : \",\";\n\n let onPluginsReadyProp: string;\n if (extendArg) {\n onPluginsReadyProp =\n `${needsComma}\\n onPluginsReady(${varName}) {\\n` +\n ` ${varName}.server.extend(${extendArg});\\n` +\n \" },\";\n } else {\n onPluginsReadyProp = \"\";\n }\n\n const newConfig = `${beforeLastBrace}${onPluginsReadyProp}\\n}`;\n const newCreateApp = `await createApp(${newConfig});`;\n\n // Replace: remove const declaration, replace with plain await, remove extend + start\n let result = content.slice(0, matchIdx) + newCreateApp;\n let remaining = afterCreateApp;\n\n if (extendFullStatement) {\n remaining = remaining.replace(extendFullStatement, \"\");\n }\n remaining = remaining.replace(startFullStatement, \"\");\n\n // Clean up consecutive blank lines\n remaining = remaining.replace(/\\n\\s*\\n\\s*\\n/g, \"\\n\\n\");\n\n result += remaining;\n\n return { migrated: true, content: result, warnings };\n}\n\nexport function migrateFile(filePath: string): MigrationResult {\n const original = fs.readFileSync(filePath, \"utf-8\");\n\n if (isAlreadyMigrated(original)) {\n return {\n migrated: false,\n content: original,\n warnings: [\"Already migrated -- no changes needed.\"],\n };\n }\n\n const content = stripAutoStartFromServerCalls(original);\n const allWarnings: string[] = [];\n\n // Try Pattern A first\n const patternA = migratePatternA(content);\n if (patternA.migrated) {\n allWarnings.push(...patternA.warnings);\n return {\n migrated: true,\n content: patternA.content,\n warnings: allWarnings,\n };\n }\n allWarnings.push(...patternA.warnings);\n\n // Try Pattern B\n const patternB = migratePatternB(content);\n if (patternB.migrated) {\n allWarnings.push(...patternB.warnings);\n return {\n migrated: true,\n content: patternB.content,\n warnings: allWarnings,\n };\n }\n allWarnings.push(...patternB.warnings);\n\n // Check if autoStart was stripped (content changed but no pattern matched)\n if (content !== original) {\n return { migrated: true, content, warnings: allWarnings };\n }\n\n return { migrated: false, content: original, warnings: allWarnings };\n}\n\nfunction runCodemod(options: { path?: string; write?: boolean }) {\n const rootDir = process.cwd();\n const write = options.write ?? false;\n\n let files: string[];\n if (options.path) {\n const absPath = path.resolve(rootDir, options.path);\n if (!fs.existsSync(absPath)) {\n console.error(`File not found: ${absPath}`);\n process.exit(1);\n }\n files = [absPath];\n } else {\n files = findServerEntryFiles(rootDir);\n }\n\n if (files.length === 0) {\n console.log(\"No files found importing createApp from @databricks/appkit.\");\n console.log(\"Use --path to specify a file explicitly.\");\n process.exit(0);\n }\n\n let hasChanges = false;\n\n for (const file of files) {\n const relPath = path.relative(rootDir, file);\n const result = migrateFile(file);\n\n for (const warning of result.warnings) {\n console.log(` ${relPath}: ${warning}`);\n }\n\n if (!result.migrated) {\n if (result.warnings.length === 0) {\n console.log(` ${relPath}: No migration needed.`);\n }\n continue;\n }\n\n hasChanges = true;\n\n if (write) {\n fs.writeFileSync(file, result.content, \"utf-8\");\n console.log(` ${relPath}: Migrated successfully.`);\n } else {\n console.log(`\\n--- ${relPath} (dry run) ---`);\n console.log(result.content);\n console.log(\"---\");\n }\n }\n\n if (hasChanges && !write) {\n console.log(\"\\nDry run complete. Run with --write to apply changes.\");\n }\n}\n\nexport const onPluginsReadyCommand = new Command(\"on-plugins-ready\")\n .description(\n \"Migrate createApp usage from autoStart/extend/start pattern to onPluginsReady callback\",\n )\n .option(\"--path <file>\", \"Path to the server entry file to migrate\")\n .option(\"--write\", \"Apply changes (default: dry-run)\", false)\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit codemod on-plugins-ready # dry-run, auto-detect files\n $ appkit codemod on-plugins-ready --write # apply changes\n $ appkit codemod on-plugins-ready --path server.ts # migrate a specific file`,\n )\n .action(runCodemod);\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"docs.js","names":[],"sources":["../../../src/cli/commands/docs.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { Command } from \"commander\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nconst COLLAPSED_MARKER = \"[collapsed]\";\n\ninterface Section {\n name: string;\n body: string;\n collapsed: boolean;\n}\n\nfunction findPackageRoot(): string {\n let dir = __dirname;\n while (dir !== path.parse(dir).root) {\n if (fs.existsSync(path.join(dir, \"package.json\"))) {\n return dir;\n }\n dir = path.dirname(dir);\n }\n throw new Error(\"Could not find package root\");\n}\n\nfunction parseSections(content: string): {\n header: string;\n sections: Section[];\n} {\n const parts = content.split(/^(## .+)$/m);\n const header = parts[0];\n const sections: Section[] = [];\n\n for (let i = 1; i < parts.length; i += 2) {\n const rawName = parts[i].replace(/^## /, \"\");\n const body = parts[i + 1] ?? \"\";\n const collapsed = rawName.includes(COLLAPSED_MARKER);\n const name = rawName.replace(COLLAPSED_MARKER, \"\").trim();\n sections.push({ name, body, collapsed });\n }\n\n return { header, sections };\n}\n\nfunction countPages(body: string): number {\n return (body.match(/^- \\[/gm) || []).length;\n}\n\nfunction findSections(sections: Section[], query: string): Section[] {\n const q = query.toLowerCase();\n return sections.filter((s) => s.name.toLowerCase().includes(q));\n}\n\nfunction isFilePath(arg: string): boolean {\n return arg.startsWith(\"./\") && arg.endsWith(\".md\");\n}\n\nfunction readLlmsTxt(packageRoot: string): string {\n const llmsPath = path.join(packageRoot, \"llms.txt\");\n if (!fs.existsSync(llmsPath)) {\n console.error(\"Error: llms.txt not found in package\");\n process.exit(1);\n }\n return fs.readFileSync(llmsPath, \"utf-8\");\n}\n\nfunction readDocFile(packageRoot: string, docPath: string): void {\n let normalizedPath = docPath;\n normalizedPath = normalizedPath.replace(/^\\.\\//, \"\");\n normalizedPath = normalizedPath.replace(/^\\//, \"\");\n normalizedPath = normalizedPath.replace(/^appkit\\//, \"\");\n\n const fullPath = path.join(packageRoot, normalizedPath);\n\n if (!fs.existsSync(fullPath)) {\n console.error(`Error: Documentation file not found: ${docPath}`);\n console.error(`Tried: ${fullPath}`);\n process.exit(1);\n }\n\n console.log(fs.readFileSync(fullPath, \"utf-8\"));\n}\n\nfunction formatCollapsedSection(section: Section): string {\n const pages = countPages(section.body);\n return [\n `## ${section.name} (${pages} pages)`,\n \"\",\n `> Use \\`appkit docs \"${section.name}\"\\` to expand, or \\`appkit docs --full\\` to expand all sections.`,\n \"\",\n ].join(\"\\n\");\n}\n\nfunction formatExpandedSection(section: Section): string {\n return `## ${section.name}${section.body}`;\n}\n\nfunction runDocs(query: string | undefined, options: { full?: boolean }) {\n const packageRoot = findPackageRoot();\n\n if (query && isFilePath(query)) {\n readDocFile(packageRoot, query);\n return;\n }\n\n const content = readLlmsTxt(packageRoot);\n\n if (options.full) {\n console.log(content.replaceAll(` ${COLLAPSED_MARKER}`, \"\"));\n return;\n }\n\n const { header, sections } = parseSections(content);\n\n if (query) {\n const matched = findSections(sections, query);\n if (matched.length === 0) {\n const available = sections.map((s) => ` - ${s.name}`).join(\"\\n\");\n console.error(\n `No section matching \"${query}\". Available sections:\\n${available}`,\n );\n process.exit(1);\n }\n console.log(matched.map(formatExpandedSection).join(\"\\n\"));\n return;\n }\n\n const output =\n header +\n sections\n .map((s) =>\n s.collapsed ? formatCollapsedSection(s) : formatExpandedSection(s),\n )\n .join(\"\\n\");\n console.log(output);\n}\n\nexport const docsCommand = new Command(\"docs\")\n .description(\"Display embedded documentation\")\n .argument(\n \"[query]\",\n \"Section name (e.g. 'plugins') or path to a doc file (e.g. './docs.md')\",\n )\n .option(\"--full\", \"Show complete index including all API reference entries\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit docs\n $ appkit docs plugins\n $ appkit docs \"appkit-ui API reference\"\n $ appkit docs ./docs/plugins/analytics.md\n $ appkit docs --full`,\n )\n .action(runDocs);\n"],"mappings":"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"}
1
+ {"version":3,"file":"docs.js","names":[],"sources":["../../../src/cli/commands/docs.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { Command } from \"commander\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nconst COLLAPSED_MARKER = \"[collapsed]\";\n\ninterface Section {\n name: string;\n body: string;\n collapsed: boolean;\n}\n\nfunction findPackageRoot(): string {\n let dir = __dirname;\n while (dir !== path.parse(dir).root) {\n if (fs.existsSync(path.join(dir, \"package.json\"))) {\n return dir;\n }\n dir = path.dirname(dir);\n }\n throw new Error(\"Could not find package root\");\n}\n\nfunction parseSections(content: string): {\n header: string;\n sections: Section[];\n} {\n const parts = content.split(/^(## .+)$/m);\n const header = parts[0];\n const sections: Section[] = [];\n\n for (let i = 1; i < parts.length; i += 2) {\n const rawName = parts[i].replace(/^## /, \"\");\n const body = parts[i + 1] ?? \"\";\n const collapsed = rawName.includes(COLLAPSED_MARKER);\n const name = rawName.replace(COLLAPSED_MARKER, \"\").trim();\n sections.push({ name, body, collapsed });\n }\n\n return { header, sections };\n}\n\nfunction countPages(body: string): number {\n return (body.match(/^- \\[/gm) || []).length;\n}\n\nfunction findSections(sections: Section[], query: string): Section[] {\n const q = query.toLowerCase();\n return sections.filter((s) => s.name.toLowerCase().includes(q));\n}\n\nfunction isFilePath(arg: string): boolean {\n return arg.startsWith(\"./\") && arg.endsWith(\".md\");\n}\n\nfunction readLlmsTxt(packageRoot: string): string {\n const llmsPath = path.join(packageRoot, \"llms.txt\");\n if (!fs.existsSync(llmsPath)) {\n console.error(\"Error: llms.txt not found in package\");\n process.exit(1);\n }\n return fs.readFileSync(llmsPath, \"utf-8\");\n}\n\nfunction readDocFile(packageRoot: string, docPath: string): void {\n let normalizedPath = docPath;\n normalizedPath = normalizedPath.replace(/^\\.\\//, \"\");\n normalizedPath = normalizedPath.replace(/^\\//, \"\");\n normalizedPath = normalizedPath.replace(/^appkit\\//, \"\");\n\n const fullPath = path.join(packageRoot, normalizedPath);\n\n if (!fs.existsSync(fullPath)) {\n console.error(`Error: Documentation file not found: ${docPath}`);\n console.error(`Tried: ${fullPath}`);\n process.exit(1);\n }\n\n console.log(fs.readFileSync(fullPath, \"utf-8\"));\n}\n\nfunction formatCollapsedSection(section: Section): string {\n const pages = countPages(section.body);\n return [\n `## ${section.name} (${pages} pages)`,\n \"\",\n `> Use \\`appkit docs \"${section.name}\"\\` to expand, or \\`appkit docs --full\\` to expand all sections.`,\n \"\",\n ].join(\"\\n\");\n}\n\nfunction formatExpandedSection(section: Section): string {\n return `## ${section.name}${section.body}`;\n}\n\nfunction runDocs(query: string | undefined, options: { full?: boolean }) {\n const packageRoot = findPackageRoot();\n\n if (query && isFilePath(query)) {\n readDocFile(packageRoot, query);\n return;\n }\n\n const content = readLlmsTxt(packageRoot);\n\n if (options.full) {\n console.log(content.replaceAll(` ${COLLAPSED_MARKER}`, \"\"));\n return;\n }\n\n const { header, sections } = parseSections(content);\n\n if (query) {\n const matched = findSections(sections, query);\n if (matched.length === 0) {\n const available = sections.map((s) => ` - ${s.name}`).join(\"\\n\");\n console.error(\n `No section matching \"${query}\". Available sections:\\n${available}`,\n );\n process.exit(1);\n }\n console.log(matched.map(formatExpandedSection).join(\"\\n\"));\n return;\n }\n\n const output =\n header +\n sections\n .map((s) =>\n s.collapsed ? formatCollapsedSection(s) : formatExpandedSection(s),\n )\n .join(\"\\n\");\n console.log(output);\n}\n\nexport const docsCommand = new Command(\"docs\")\n .description(\"Display embedded documentation\")\n .argument(\n \"[query]\",\n \"Section name (e.g. 'plugins') or path to a doc file (e.g. './docs.md')\",\n )\n .option(\"--full\", \"Show complete index including all API reference entries\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit docs\n $ appkit docs plugins\n $ appkit docs \"appkit-ui API reference\"\n $ appkit docs ./docs/plugins/analytics.md\n $ appkit docs --full`,\n )\n .action(runDocs);\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"bundle.js","names":[],"sources":["../../../../src/cli/commands/doctor/bundle.ts"],"sourcesContent":["/**\n * Reads `databricks.yml` + `app.yaml` for two things doctor can't get from\n * `appkit.plugins.json`:\n *\n * 1. **Provenance** — whether each binding references an existing resource\n * (`${var.*}`/literal → `external`) or one this bundle creates\n * (`${resources.<type>.<key>.*}` → `bundle-managed`).\n * 2. **Wiring** — the binding `name` joins the three files: `app.yaml`'s\n * `valueFrom: <name>` must match a `databricks.yml` binding `name`.\n *\n * SDK-free; pure YAML parsing. Absent files degrade to empty results.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport yaml from \"js-yaml\";\nimport {\n APP_YAML_FILE,\n bindingTypeOf,\n DATABRICKS_YML_FILE,\n} from \"../../deploy-config\";\nimport type { ResourceOrigin } from \"./types\";\nimport { errorMessage } from \"./utils\";\n\n/** A `${resources.<type>.<key>.<field>}` reference — a bundle-created resource. */\nconst RESOURCES_REF = /\\$\\{resources\\.([^.]+)\\.([^.}]+)\\.[^}]+\\}/;\n\n/** One app resource binding from `resources.apps.<app>.resources[]`. */\nexport interface BundleBinding {\n /** The binding name — the join key with `app.yaml` `valueFrom`. */\n name: string;\n /** The typed sub-key that names the kind (`sql_warehouse`, `genie_space`, …). */\n type?: string;\n origin: ResourceOrigin;\n /** For bundle-managed bindings: the `<type>.<key>` it references, if parseable. */\n ref?: { type: string; key: string };\n}\n\nexport interface BundleInfo {\n /** Bindings under every app, keyed by binding name. */\n bindings: Map<string, BundleBinding>;\n /** `app.yaml` env var name → binding name (from `valueFrom`). */\n envToBinding: Map<string, string>;\n /** Declared bundle resource keys, as `\"<type>.<key>\"`, for ref-integrity. */\n declaredResources: Set<string>;\n /** True when a `databricks.yml` was found and parsed. */\n present: boolean;\n}\n\ninterface AppResourceBlock {\n name?: string;\n [k: string]: unknown;\n}\ninterface BundleAppBlock {\n resources?: AppResourceBlock[];\n}\ninterface BundleDoc {\n resources?: {\n apps?: Record<string, BundleAppBlock>;\n [otherType: string]: Record<string, unknown> | undefined;\n };\n}\ninterface AppYamlDoc {\n env?: Array<{ name?: string; valueFrom?: string }>;\n}\n\n/** Classifies a binding by its typed sub-key and origin: scanning its field\n * values for a `${resources.*}` reference (bundle-managed) vs anything else\n * (external). */\nfunction classifyBinding(block: AppResourceBlock): {\n type?: string;\n origin: ResourceOrigin;\n ref?: { type: string; key: string };\n} {\n const type = bindingTypeOf(block);\n const typed = type ? block[type] : undefined;\n if (typed && typeof typed === \"object\") {\n for (const value of Object.values(typed as Record<string, unknown>)) {\n if (typeof value !== \"string\") continue;\n const m = value.match(RESOURCES_REF);\n if (m) {\n return {\n type,\n origin: \"bundle-managed\",\n ref: { type: m[1], key: m[2] },\n };\n }\n }\n }\n return { type, origin: \"external\" };\n}\n\n/**\n * Reads and parses a YAML file. Returns `null` when the file is absent (a\n * legitimate \"no bundle\" state); throws on invalid YAML, since silently\n * ignoring it would let doctor report a false all-clear.\n */\nfunction readYaml<T>(filePath: string): T | null {\n let raw: string;\n try {\n raw = fs.readFileSync(filePath, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw err;\n }\n try {\n return (yaml.load(raw) ?? {}) as T;\n } catch (err) {\n throw new Error(\n `Failed to parse ${path.basename(filePath)}: ${errorMessage(err)}`,\n );\n }\n}\n\n/**\n * Parses `databricks.yml` (+ `app.yaml`) into {@link BundleInfo}. Returns an\n * empty-but-`present:false` result when no bundle is found, so callers can treat\n * \"no bundle\" as \"everything external\" without special-casing.\n */\nexport function readBundleInfo(\n cwd: string = process.cwd(),\n bundleFile: string = DATABRICKS_YML_FILE,\n appYamlFile: string = APP_YAML_FILE,\n): BundleInfo {\n const bindings = new Map<string, BundleBinding>();\n const envToBinding = new Map<string, string>();\n const declaredResources = new Set<string>();\n\n const doc = readYaml<BundleDoc>(path.resolve(cwd, bundleFile));\n if (!doc) {\n return { bindings, envToBinding, declaredResources, present: false };\n }\n\n for (const [type, group] of Object.entries(doc.resources ?? {})) {\n if (!group || typeof group !== \"object\") continue;\n for (const key of Object.keys(group))\n declaredResources.add(`${type}.${key}`);\n }\n\n for (const app of Object.values(doc.resources?.apps ?? {})) {\n for (const block of app.resources ?? []) {\n if (!block?.name) continue;\n const { type, origin, ref } = classifyBinding(block);\n bindings.set(block.name, {\n name: block.name,\n type,\n origin,\n ref,\n });\n }\n }\n\n const appYaml = readYaml<AppYamlDoc>(path.resolve(cwd, appYamlFile));\n for (const entry of appYaml?.env ?? []) {\n if (entry?.name && entry.valueFrom) {\n envToBinding.set(entry.name, entry.valueFrom);\n }\n }\n\n return { bindings, envToBinding, declaredResources, present: true };\n}\n\n/**\n * Resolves the origin for a target given its env vars, by walking\n * env var → binding name → binding origin. Returns undefined when the bundle\n * has nothing to say (so the target stays external by default).\n */\nexport function originForEnvVars(\n envVars: string[],\n info: BundleInfo,\n): ResourceOrigin | undefined {\n if (!info.present) return undefined;\n for (const env of envVars) {\n const bindingName = info.envToBinding.get(env);\n if (!bindingName) continue;\n const binding = info.bindings.get(bindingName);\n if (binding) return binding.origin;\n }\n return undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyBA,MAAM,gBAAgB;;;;AA4CtB,SAAS,gBAAgB,OAIvB;CACA,MAAM,OAAO,cAAc,MAAM;CACjC,MAAM,QAAQ,OAAO,MAAM,QAAQ;AACnC,KAAI,SAAS,OAAO,UAAU,SAC5B,MAAK,MAAM,SAAS,OAAO,OAAO,MAAiC,EAAE;AACnE,MAAI,OAAO,UAAU,SAAU;EAC/B,MAAM,IAAI,MAAM,MAAM,cAAc;AACpC,MAAI,EACF,QAAO;GACL;GACA,QAAQ;GACR,KAAK;IAAE,MAAM,EAAE;IAAI,KAAK,EAAE;IAAI;GAC/B;;AAIP,QAAO;EAAE;EAAM,QAAQ;EAAY;;;;;;;AAQrC,SAAS,SAAY,UAA4B;CAC/C,IAAI;AACJ,KAAI;AACF,QAAM,GAAG,aAAa,UAAU,QAAQ;UACjC,KAAK;AACZ,MAAK,IAA8B,SAAS,SAAU,QAAO;AAC7D,QAAM;;AAER,KAAI;AACF,SAAQ,KAAK,KAAK,IAAI,IAAI,EAAE;UACrB,KAAK;AACZ,QAAM,IAAI,MACR,mBAAmB,KAAK,SAAS,SAAS,CAAC,IAAI,aAAa,IAAI,GACjE;;;;;;;;AASL,SAAgB,eACd,MAAc,QAAQ,KAAK,EAC3B,aAAqB,qBACrB,cAAsB,eACV;CACZ,MAAM,2BAAW,IAAI,KAA4B;CACjD,MAAM,+BAAe,IAAI,KAAqB;CAC9C,MAAM,oCAAoB,IAAI,KAAa;CAE3C,MAAM,MAAM,SAAoB,KAAK,QAAQ,KAAK,WAAW,CAAC;AAC9D,KAAI,CAAC,IACH,QAAO;EAAE;EAAU;EAAc;EAAmB,SAAS;EAAO;AAGtE,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,IAAI,aAAa,EAAE,CAAC,EAAE;AAC/D,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU;AACzC,OAAK,MAAM,OAAO,OAAO,KAAK,MAAM,CAClC,mBAAkB,IAAI,GAAG,KAAK,GAAG,MAAM;;AAG3C,MAAK,MAAM,OAAO,OAAO,OAAO,IAAI,WAAW,QAAQ,EAAE,CAAC,CACxD,MAAK,MAAM,SAAS,IAAI,aAAa,EAAE,EAAE;AACvC,MAAI,CAAC,OAAO,KAAM;EAClB,MAAM,EAAE,MAAM,QAAQ,QAAQ,gBAAgB,MAAM;AACpD,WAAS,IAAI,MAAM,MAAM;GACvB,MAAM,MAAM;GACZ;GACA;GACA;GACD,CAAC;;CAIN,MAAM,UAAU,SAAqB,KAAK,QAAQ,KAAK,YAAY,CAAC;AACpE,MAAK,MAAM,SAAS,SAAS,OAAO,EAAE,CACpC,KAAI,OAAO,QAAQ,MAAM,UACvB,cAAa,IAAI,MAAM,MAAM,MAAM,UAAU;AAIjD,QAAO;EAAE;EAAU;EAAc;EAAmB,SAAS;EAAM;;;;;;;AAQrE,SAAgB,iBACd,SACA,MAC4B;AAC5B,KAAI,CAAC,KAAK,QAAS,QAAO;AAC1B,MAAK,MAAM,OAAO,SAAS;EACzB,MAAM,cAAc,KAAK,aAAa,IAAI,IAAI;AAC9C,MAAI,CAAC,YAAa;EAClB,MAAM,UAAU,KAAK,SAAS,IAAI,YAAY;AAC9C,MAAI,QAAS,QAAO,QAAQ"}
1
+ {"version":3,"file":"bundle.js","names":[],"sources":["../../../../src/cli/commands/doctor/bundle.ts"],"sourcesContent":["/**\n * Reads `databricks.yml` + `app.yaml` for two things doctor can't get from\n * `appkit.plugins.json`:\n *\n * 1. **Provenance** — whether each binding references an existing resource\n * (`${var.*}`/literal → `external`) or one this bundle creates\n * (`${resources.<type>.<key>.*}` → `bundle-managed`).\n * 2. **Wiring** — the binding `name` joins the three files: `app.yaml`'s\n * `valueFrom: <name>` must match a `databricks.yml` binding `name`.\n *\n * SDK-free; pure YAML parsing. Absent files degrade to empty results.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport yaml from \"js-yaml\";\n\nimport {\n APP_YAML_FILE,\n bindingTypeOf,\n DATABRICKS_YML_FILE,\n} from \"../../deploy-config\";\nimport type { ResourceOrigin } from \"./types\";\nimport { errorMessage } from \"./utils\";\n\n/** A `${resources.<type>.<key>.<field>}` reference — a bundle-created resource. */\nconst RESOURCES_REF = /\\$\\{resources\\.([^.]+)\\.([^.}]+)\\.[^}]+\\}/;\n\n/** One app resource binding from `resources.apps.<app>.resources[]`. */\nexport interface BundleBinding {\n /** The binding name — the join key with `app.yaml` `valueFrom`. */\n name: string;\n /** The typed sub-key that names the kind (`sql_warehouse`, `genie_space`, …). */\n type?: string;\n origin: ResourceOrigin;\n /** For bundle-managed bindings: the `<type>.<key>` it references, if parseable. */\n ref?: { type: string; key: string };\n}\n\nexport interface BundleInfo {\n /** Bindings under every app, keyed by binding name. */\n bindings: Map<string, BundleBinding>;\n /** `app.yaml` env var name → binding name (from `valueFrom`). */\n envToBinding: Map<string, string>;\n /** Declared bundle resource keys, as `\"<type>.<key>\"`, for ref-integrity. */\n declaredResources: Set<string>;\n /** True when a `databricks.yml` was found and parsed. */\n present: boolean;\n}\n\ninterface AppResourceBlock {\n name?: string;\n [k: string]: unknown;\n}\ninterface BundleAppBlock {\n resources?: AppResourceBlock[];\n}\ninterface BundleDoc {\n resources?: {\n apps?: Record<string, BundleAppBlock>;\n [otherType: string]: Record<string, unknown> | undefined;\n };\n}\ninterface AppYamlDoc {\n env?: Array<{ name?: string; valueFrom?: string }>;\n}\n\n/** Classifies a binding by its typed sub-key and origin: scanning its field\n * values for a `${resources.*}` reference (bundle-managed) vs anything else\n * (external). */\nfunction classifyBinding(block: AppResourceBlock): {\n type?: string;\n origin: ResourceOrigin;\n ref?: { type: string; key: string };\n} {\n const type = bindingTypeOf(block);\n const typed = type ? block[type] : undefined;\n if (typed && typeof typed === \"object\") {\n for (const value of Object.values(typed as Record<string, unknown>)) {\n if (typeof value !== \"string\") continue;\n const m = value.match(RESOURCES_REF);\n if (m) {\n return {\n type,\n origin: \"bundle-managed\",\n ref: { type: m[1], key: m[2] },\n };\n }\n }\n }\n return { type, origin: \"external\" };\n}\n\n/**\n * Reads and parses a YAML file. Returns `null` when the file is absent (a\n * legitimate \"no bundle\" state); throws on invalid YAML, since silently\n * ignoring it would let doctor report a false all-clear.\n */\nfunction readYaml<T>(filePath: string): T | null {\n let raw: string;\n try {\n raw = fs.readFileSync(filePath, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw err;\n }\n try {\n return (yaml.load(raw) ?? {}) as T;\n } catch (err) {\n throw new Error(\n `Failed to parse ${path.basename(filePath)}: ${errorMessage(err)}`,\n );\n }\n}\n\n/**\n * Parses `databricks.yml` (+ `app.yaml`) into {@link BundleInfo}. Returns an\n * empty-but-`present:false` result when no bundle is found, so callers can treat\n * \"no bundle\" as \"everything external\" without special-casing.\n */\nexport function readBundleInfo(\n cwd: string = process.cwd(),\n bundleFile: string = DATABRICKS_YML_FILE,\n appYamlFile: string = APP_YAML_FILE,\n): BundleInfo {\n const bindings = new Map<string, BundleBinding>();\n const envToBinding = new Map<string, string>();\n const declaredResources = new Set<string>();\n\n const doc = readYaml<BundleDoc>(path.resolve(cwd, bundleFile));\n if (!doc) {\n return { bindings, envToBinding, declaredResources, present: false };\n }\n\n for (const [type, group] of Object.entries(doc.resources ?? {})) {\n if (!group || typeof group !== \"object\") continue;\n for (const key of Object.keys(group))\n declaredResources.add(`${type}.${key}`);\n }\n\n for (const app of Object.values(doc.resources?.apps ?? {})) {\n for (const block of app.resources ?? []) {\n if (!block?.name) continue;\n const { type, origin, ref } = classifyBinding(block);\n bindings.set(block.name, {\n name: block.name,\n type,\n origin,\n ref,\n });\n }\n }\n\n const appYaml = readYaml<AppYamlDoc>(path.resolve(cwd, appYamlFile));\n for (const entry of appYaml?.env ?? []) {\n if (entry?.name && entry.valueFrom) {\n envToBinding.set(entry.name, entry.valueFrom);\n }\n }\n\n return { bindings, envToBinding, declaredResources, present: true };\n}\n\n/**\n * Resolves the origin for a target given its env vars, by walking\n * env var → binding name → binding origin. Returns undefined when the bundle\n * has nothing to say (so the target stays external by default).\n */\nexport function originForEnvVars(\n envVars: string[],\n info: BundleInfo,\n): ResourceOrigin | undefined {\n if (!info.present) return undefined;\n for (const env of envVars) {\n const bindingName = info.envToBinding.get(env);\n if (!bindingName) continue;\n const binding = info.bindings.get(bindingName);\n if (binding) return binding.origin;\n }\n return undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA2BA,MAAM,gBAAgB;;;;AA4CtB,SAAS,gBAAgB,OAIvB;CACA,MAAM,OAAO,cAAc,MAAM;CACjC,MAAM,QAAQ,OAAO,MAAM,QAAQ;AACnC,KAAI,SAAS,OAAO,UAAU,SAC5B,MAAK,MAAM,SAAS,OAAO,OAAO,MAAiC,EAAE;AACnE,MAAI,OAAO,UAAU,SAAU;EAC/B,MAAM,IAAI,MAAM,MAAM,cAAc;AACpC,MAAI,EACF,QAAO;GACL;GACA,QAAQ;GACR,KAAK;IAAE,MAAM,EAAE;IAAI,KAAK,EAAE;IAAI;GAC/B;;AAIP,QAAO;EAAE;EAAM,QAAQ;EAAY;;;;;;;AAQrC,SAAS,SAAY,UAA4B;CAC/C,IAAI;AACJ,KAAI;AACF,QAAM,GAAG,aAAa,UAAU,QAAQ;UACjC,KAAK;AACZ,MAAK,IAA8B,SAAS,SAAU,QAAO;AAC7D,QAAM;;AAER,KAAI;AACF,SAAQ,KAAK,KAAK,IAAI,IAAI,EAAE;UACrB,KAAK;AACZ,QAAM,IAAI,MACR,mBAAmB,KAAK,SAAS,SAAS,CAAC,IAAI,aAAa,IAAI,GACjE;;;;;;;;AASL,SAAgB,eACd,MAAc,QAAQ,KAAK,EAC3B,aAAqB,qBACrB,cAAsB,eACV;CACZ,MAAM,2BAAW,IAAI,KAA4B;CACjD,MAAM,+BAAe,IAAI,KAAqB;CAC9C,MAAM,oCAAoB,IAAI,KAAa;CAE3C,MAAM,MAAM,SAAoB,KAAK,QAAQ,KAAK,WAAW,CAAC;AAC9D,KAAI,CAAC,IACH,QAAO;EAAE;EAAU;EAAc;EAAmB,SAAS;EAAO;AAGtE,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,IAAI,aAAa,EAAE,CAAC,EAAE;AAC/D,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU;AACzC,OAAK,MAAM,OAAO,OAAO,KAAK,MAAM,CAClC,mBAAkB,IAAI,GAAG,KAAK,GAAG,MAAM;;AAG3C,MAAK,MAAM,OAAO,OAAO,OAAO,IAAI,WAAW,QAAQ,EAAE,CAAC,CACxD,MAAK,MAAM,SAAS,IAAI,aAAa,EAAE,EAAE;AACvC,MAAI,CAAC,OAAO,KAAM;EAClB,MAAM,EAAE,MAAM,QAAQ,QAAQ,gBAAgB,MAAM;AACpD,WAAS,IAAI,MAAM,MAAM;GACvB,MAAM,MAAM;GACZ;GACA;GACA;GACD,CAAC;;CAIN,MAAM,UAAU,SAAqB,KAAK,QAAQ,KAAK,YAAY,CAAC;AACpE,MAAK,MAAM,SAAS,SAAS,OAAO,EAAE,CACpC,KAAI,OAAO,QAAQ,MAAM,UACvB,cAAa,IAAI,MAAM,MAAM,MAAM,UAAU;AAIjD,QAAO;EAAE;EAAU;EAAc;EAAmB,SAAS;EAAM;;;;;;;AAQrE,SAAgB,iBACd,SACA,MAC4B;AAC5B,KAAI,CAAC,KAAK,QAAS,QAAO;AAC1B,MAAK,MAAM,OAAO,SAAS;EACzB,MAAM,cAAc,KAAK,aAAa,IAAI,IAAI;AAC9C,MAAI,CAAC,YAAa;EAClB,MAAM,UAAU,KAAK,SAAS,IAAI,YAAY;AAC9C,MAAI,QAAS,QAAO,QAAQ"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../../src/cli/commands/doctor/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport process from \"node:process\";\nimport { Command } from \"commander\";\nimport dotenv from \"dotenv\";\nimport { printReport, printReportJson } from \"./report\";\nimport { runDoctor } from \"./run\";\nimport type { DoctorOptions } from \"./types\";\n\n/**\n * Loads an explicit env file into `process.env`, overriding the auto-loaded\n * `.env`. Throws if the file is missing.\n */\nexport function loadEnvFile(envFile: string): void {\n if (!fs.existsSync(envFile)) {\n throw new Error(`env file not found: ${envFile}`);\n }\n dotenv.config({ path: envFile, override: true });\n}\n\nasync function runDoctorCommand(options: DoctorOptions): Promise<void> {\n if (options.envFile) loadEnvFile(options.envFile);\n\n const report = await runDoctor(options);\n\n if (options.json) {\n printReportJson(report, options.detail);\n } else {\n printReport(report, options.detail);\n }\n\n process.exit(report.exitCode);\n}\n\nexport const doctorCommand = new Command(\"doctor\")\n .description(\n \"Diagnose an AppKit app's Databricks resources: authentication, config, and resource existence/reachability\",\n )\n .option(\"-p, --profile <name>\", \"Databricks CLI profile to authenticate with\")\n .option(\n \"-e, --env-file <path>\",\n \"Load this env file before checking (overrides the auto-loaded .env), e.g. .env.local\",\n )\n .option(\n \"-d, --detail\",\n \"Show full underlying error messages (raw SDK output) for failures\",\n )\n .option(\"--json\", \"Output the diagnostic report as JSON\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit doctor\n $ appkit doctor --profile my-profile\n $ appkit doctor --env-file .env.local\n $ appkit doctor --detail\n $ appkit doctor --json`,\n )\n .action((opts: DoctorOptions) =>\n runDoctorCommand(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":";;;;;;;;;;;;AAYA,SAAgB,YAAY,SAAuB;AACjD,KAAI,CAAC,GAAG,WAAW,QAAQ,CACzB,OAAM,IAAI,MAAM,uBAAuB,UAAU;AAEnD,QAAO,OAAO;EAAE,MAAM;EAAS,UAAU;EAAM,CAAC;;AAGlD,eAAe,iBAAiB,SAAuC;AACrE,KAAI,QAAQ,QAAS,aAAY,QAAQ,QAAQ;CAEjD,MAAM,SAAS,MAAM,UAAU,QAAQ;AAEvC,KAAI,QAAQ,KACV,iBAAgB,QAAQ,QAAQ,OAAO;KAEvC,aAAY,QAAQ,QAAQ,OAAO;AAGrC,SAAQ,KAAK,OAAO,SAAS;;AAG/B,MAAa,gBAAgB,IAAI,QAAQ,SAAS,CAC/C,YACC,6GACD,CACA,OAAO,wBAAwB,8CAA8C,CAC7E,OACC,yBACA,uFACD,CACA,OACC,gBACA,oEACD,CACA,OAAO,UAAU,uCAAuC,CACxD,YACC,SACA;;;;;;0BAOD,CACA,QAAQ,SACP,iBAAiB,KAAK,CAAC,OAAO,QAAQ;AACpC,SAAQ,MAAM,IAAI;AAClB,SAAQ,KAAK,EAAE;EACf,CACH"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../src/cli/commands/doctor/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport process from \"node:process\";\n\nimport { Command } from \"commander\";\nimport dotenv from \"dotenv\";\n\nimport { printReport, printReportJson } from \"./report\";\nimport { runDoctor } from \"./run\";\nimport type { DoctorOptions } from \"./types\";\n\n/**\n * Loads an explicit env file into `process.env`, overriding the auto-loaded\n * `.env`. Throws if the file is missing.\n */\nexport function loadEnvFile(envFile: string): void {\n if (!fs.existsSync(envFile)) {\n throw new Error(`env file not found: ${envFile}`);\n }\n dotenv.config({ path: envFile, override: true });\n}\n\nasync function runDoctorCommand(options: DoctorOptions): Promise<void> {\n if (options.envFile) loadEnvFile(options.envFile);\n\n const report = await runDoctor(options);\n\n if (options.json) {\n printReportJson(report, options.detail);\n } else {\n printReport(report, options.detail);\n }\n\n process.exit(report.exitCode);\n}\n\nexport const doctorCommand = new Command(\"doctor\")\n .description(\n \"Diagnose an AppKit app's Databricks resources: authentication, config, and resource existence/reachability\",\n )\n .option(\"-p, --profile <name>\", \"Databricks CLI profile to authenticate with\")\n .option(\n \"-e, --env-file <path>\",\n \"Load this env file before checking (overrides the auto-loaded .env), e.g. .env.local\",\n )\n .option(\n \"-d, --detail\",\n \"Show full underlying error messages (raw SDK output) for failures\",\n )\n .option(\"--json\", \"Output the diagnostic report as JSON\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit doctor\n $ appkit doctor --profile my-profile\n $ appkit doctor --env-file .env.local\n $ appkit doctor --detail\n $ appkit doctor --json`,\n )\n .action((opts: DoctorOptions) =>\n runDoctorCommand(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":";;;;;;;;;;;;AAcA,SAAgB,YAAY,SAAuB;AACjD,KAAI,CAAC,GAAG,WAAW,QAAQ,CACzB,OAAM,IAAI,MAAM,uBAAuB,UAAU;AAEnD,QAAO,OAAO;EAAE,MAAM;EAAS,UAAU;EAAM,CAAC;;AAGlD,eAAe,iBAAiB,SAAuC;AACrE,KAAI,QAAQ,QAAS,aAAY,QAAQ,QAAQ;CAEjD,MAAM,SAAS,MAAM,UAAU,QAAQ;AAEvC,KAAI,QAAQ,KACV,iBAAgB,QAAQ,QAAQ,OAAO;KAEvC,aAAY,QAAQ,QAAQ,OAAO;AAGrC,SAAQ,KAAK,OAAO,SAAS;;AAG/B,MAAa,gBAAgB,IAAI,QAAQ,SAAS,CAC/C,YACC,6GACD,CACA,OAAO,wBAAwB,8CAA8C,CAC7E,OACC,yBACA,uFACD,CACA,OACC,gBACA,oEACD,CACA,OAAO,UAAU,uCAAuC,CACxD,YACC,SACA;;;;;;0BAOD,CACA,QAAQ,SACP,iBAAiB,KAAK,CAAC,OAAO,QAAQ;AACpC,SAAQ,MAAM,IAAI;AAClB,SAAQ,KAAK,EAAE;EACf,CACH"}
@@ -1 +1 @@
1
- {"version":3,"file":"report.js","names":[],"sources":["../../../../src/cli/commands/doctor/report.ts"],"sourcesContent":["/**\n * Rendering for `appkit doctor` — turns a {@link DoctorReport} into a\n * human-readable console report or JSON.\n */\n\nimport pc from \"picocolors\";\nimport {\n AUTH_UNAVAILABLE_CODE,\n BUNDLE_MANAGED_CODE,\n type CheckStatus,\n type DoctorReport,\n type ResourceCheckResult,\n STATUS_SEVERITY,\n type WiringFinding,\n} from \"./types\";\n\n/** Column at which a row's label starts, to align detail/hint lines beneath. */\nconst SUB_INDENT = \" \";\n\n/**\n * True when a resource's only finding is an auth-skipped existence probe, so it\n * can be collapsed into one summary line.\n */\nfunction isOnlyAuthSkipped(r: ResourceCheckResult): boolean {\n if (r.status !== \"skipped\") return false;\n const hasAuthSkip = r.layers.some(\n (l) => l.layer === \"existence\" && l.code === AUTH_UNAVAILABLE_CODE,\n );\n if (!hasAuthSkip) return false;\n return r.layers.every(\n (l) =>\n l.status === \"ok\" ||\n (l.layer === \"existence\" && l.code === AUTH_UNAVAILABLE_CODE),\n );\n}\n\n/** Left-pads every line of `text` by `spaces`, for indented detail blocks. */\nfunction indent(text: string, spaces: number): string {\n const pad = \" \".repeat(spaces);\n return text\n .split(\"\\n\")\n .map((l) => `${pad}${l}`)\n .join(\"\\n\");\n}\n\n/** A status glyph, coloured by severity. */\nfunction glyph(status: CheckStatus): string {\n switch (status) {\n case \"ok\":\n return pc.green(\"✓\");\n case \"warn\":\n return pc.yellow(\"⚠\");\n case \"error\":\n return pc.red(\"✗\");\n default:\n return pc.dim(\"•\");\n }\n}\n\n/**\n * Highlights actionable tokens in a detail/hint line: cyan for a \"quoted\" id or\n * `backticked` code span (delimiters stripped), bold for a SCREAMING_SNAKE env\n * var. Nesting is safe — bold's reset (22) doesn't clear the cyan (39).\n *\n * Bold runs *first*: the cyan passes wrap their match in ANSI escapes, which\n * would put a `\\x1b[36m` between the word boundary and the env-var name and stop\n * the `\\b` anchor matching — so an env var inside a code span or quotes would\n * never get bold.\n */\nfunction highlight(text: string): string {\n return text\n .replace(/\\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\\b/g, (id) => pc.bold(id))\n .replace(/`([^`]+)`/g, (_, code) => pc.cyan(code))\n .replace(/\"([^\"]+)\"/g, (_, id) => pc.cyan(id));\n}\n\n/** A sub-line beneath a row (detail or hint), consistently indented. */\nfunction subLine(text: string): void {\n console.log(`${SUB_INDENT}${text}`);\n}\n\n/**\n * Prints the expanded body of a non-ok row: each indented `detail` line, then\n * any `Hint:` set off by a blank line. Callers own the surrounding blank lines.\n */\nfunction printRowBody(\n details: Array<string | undefined>,\n hints: Array<string | undefined>,\n): void {\n for (const d of details) if (d) subLine(highlight(d));\n const realHints = hints.filter((h): h is string => Boolean(h));\n if (realHints.length > 0) {\n console.log(\"\");\n for (const h of realHints) subLine(`${pc.dim(\"Hint:\")} ${highlight(h)}`);\n }\n}\n\n/** Shows only the non-zero categories, coloured by severity. */\nfunction summaryLine(counts: {\n ok: number;\n warn: number;\n error: number;\n skipped: number;\n}): string {\n const parts: string[] = [];\n if (counts.error)\n parts.push(pc.red(`${counts.error} error${counts.error > 1 ? \"s\" : \"\"}`));\n if (counts.warn)\n parts.push(\n pc.yellow(`${counts.warn} warning${counts.warn > 1 ? \"s\" : \"\"}`),\n );\n if (counts.ok) parts.push(pc.green(`${counts.ok} ok`));\n if (counts.skipped) parts.push(pc.dim(`${counts.skipped} skipped`));\n return parts.length > 0 ? parts.join(pc.dim(\", \")) : \"nothing to check\";\n}\n\n/** Prints the leading `Auth` check row (always first). On failure, appends the\n * workspace and profile in play, a hint, and — with `--detail` — the raw SDK\n * error. The host matters as much as the profile: `DATABRICKS_HOST` overrides a\n * profile's host, so the two can come from different places. */\nfunction printAuthRow(report: DoctorReport, detail: boolean): void {\n const { auth } = report;\n console.log(` ${glyph(auth.status)} Auth — ${auth.detail ?? auth.status}`);\n // A warn carries a config problem (e.g. a host/profile split) whose hint needs\n // the same host/profile context as an outright failure.\n if (auth.status !== \"error\" && auth.status !== \"warn\") return;\n\n if (auth.host) {\n subLine(`${pc.dim(\"host:\")} ${pc.cyan(auth.host)}`);\n }\n if (auth.profile) {\n subLine(`${pc.dim(\"profile:\")} ${pc.cyan(auth.profile)}`);\n }\n if (auth.hint) {\n console.log(\"\");\n subLine(`${pc.dim(\"Hint:\")} ${highlight(auth.hint)}`);\n }\n if (detail && auth.raw) {\n console.log(\"\");\n subLine(pc.dim(\"Details:\"));\n console.log(pc.dim(indent(auth.raw, SUB_INDENT.length + 2)));\n }\n}\n\n/**\n * Prints one resource row: a tick and the name on the happy path, actionable\n * layer details beneath otherwise. Bundle-managed resources are noted as\n * deploy-created, not probed.\n */\nfunction printResourceRow(r: ResourceCheckResult): void {\n const { target } = r;\n const bundleManaged = target.origin === \"bundle-managed\";\n console.log(\n bundleManaged\n ? ` ${pc.dim(\"⧗\")} ${target.alias} ${pc.dim(\"will be created on deploy\")}`\n : ` ${glyph(r.status)} ${target.alias}`,\n );\n // Never swallow a finding: a bundle-managed row used to drop its layers, so an\n // error counted in the summary had no visible cause. `BUNDLE_MANAGED` is the\n // expected skip and would just restate the row, so it alone stays quiet.\n const problems = r.layers.filter(\n (l) => l.status !== \"ok\" && l.code !== BUNDLE_MANAGED_CODE,\n );\n if (problems.length > 0) {\n printRowBody(\n problems.map((l) => l.detail),\n problems.map((l) => l.hint),\n );\n }\n}\n\n/** Prints a wiring finding with the same shape as a resource row:\n * `glyph label` on the first line, then the indented detail and hint. */\nfunction printWiringRow(f: WiringFinding): void {\n console.log(` ${glyph(f.status)} ${highlight(f.label)}`);\n printRowBody([f.detail], [f.hint]);\n}\n\n/** A unified printable row. `expanded` rows get a blank line on either side;\n * plain `ok` rows pack together. */\ninterface Row {\n status: CheckStatus;\n expanded: boolean;\n print: () => void;\n}\n\nexport function printReport(report: DoctorReport, detail = false): void {\n const { resources, wiring, setup, summary, auth } = report;\n\n // One flat, severity-sorted list. Auth leads and never sorts.\n const rows: Row[] = [];\n rows.push({\n status: auth.status,\n expanded: auth.status === \"error\" || auth.status === \"warn\",\n print: () => printAuthRow(report, detail),\n });\n\n // When auth failed, resources whose only finding is the auth-skip carry no\n // new information, so collapse them into a single line.\n const authSkipped: ResourceCheckResult[] = [];\n const checked: Row[] = [];\n for (const r of resources) {\n if (isOnlyAuthSkipped(r)) {\n authSkipped.push(r);\n continue;\n }\n checked.push({\n status: r.status,\n // A bundle-managed row is a one-liner unless it carries a real finding, in\n // which case it needs the spacing every expanded row gets.\n expanded:\n r.status !== \"ok\" &&\n r.layers.some(\n (l) => l.status !== \"ok\" && l.code !== BUNDLE_MANAGED_CODE,\n ),\n print: () => printResourceRow(r),\n });\n }\n if (authSkipped.length > 0) {\n checked.push({\n status: \"skipped\",\n expanded: false,\n print: () =>\n console.log(\n ` ${pc.dim(\"•\")} ${pc.dim(\n `${authSkipped.length} resource${authSkipped.length > 1 ? \"s\" : \"\"} not checked (auth failed)`,\n )}`,\n ),\n });\n }\n for (const f of wiring) {\n checked.push({\n status: f.status,\n expanded: true,\n print: () => printWiringRow(f),\n });\n }\n // Setup notices share the wiring row shape (same `label`/`detail`/`hint`).\n for (const f of setup) {\n checked.push({\n status: f.status,\n expanded: true,\n print: () => printWiringRow(f),\n });\n }\n\n // Most severe first (descending severity); stable within a status.\n checked.sort((a, b) => STATUS_SEVERITY[b.status] - STATUS_SEVERITY[a.status]);\n rows.push(...checked);\n\n // A blank line sets any expanded row apart from its neighbours.\n console.log(\"\");\n rows.forEach((row, i) => {\n const prev = rows[i - 1];\n if (i > 0 && (row.expanded || prev.expanded)) console.log(\"\");\n row.print();\n });\n\n // `summary` already counts auth + wiring (built authoritatively in run.ts),\n // so render it directly — no re-folding, and it matches what --json emits.\n console.log(\"\");\n console.log(summaryLine(summary));\n // Point at --detail when there's more to show and it wasn't requested.\n if (!detail && auth.raw) {\n console.log(pc.dim(\"\\nRun with --detail for full error output.\"));\n }\n console.log(\"\");\n}\n\n/**\n * Emits the report as JSON. The raw SDK error (`auth.raw`) can carry sensitive\n * detail, so — matching the human report's `--detail` gate — it's stripped\n * unless `detail` is set. `--json --detail` opts back into the full error.\n */\nexport function printReportJson(report: DoctorReport, detail = false): void {\n const emitted =\n detail || report.auth.raw === undefined\n ? report\n : { ...report, auth: { ...report.auth, raw: undefined } };\n console.log(JSON.stringify(emitted, null, 2));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"report.js","names":[],"sources":["../../../../src/cli/commands/doctor/report.ts"],"sourcesContent":["/**\n * Rendering for `appkit doctor` — turns a {@link DoctorReport} into a\n * human-readable console report or JSON.\n */\n\nimport pc from \"picocolors\";\n\nimport {\n AUTH_UNAVAILABLE_CODE,\n BUNDLE_MANAGED_CODE,\n type CheckStatus,\n type DoctorReport,\n type ResourceCheckResult,\n STATUS_SEVERITY,\n type WiringFinding,\n} from \"./types\";\n\n/** Column at which a row's label starts, to align detail/hint lines beneath. */\nconst SUB_INDENT = \" \";\n\n/**\n * True when a resource's only finding is an auth-skipped existence probe, so it\n * can be collapsed into one summary line.\n */\nfunction isOnlyAuthSkipped(r: ResourceCheckResult): boolean {\n if (r.status !== \"skipped\") return false;\n const hasAuthSkip = r.layers.some(\n (l) => l.layer === \"existence\" && l.code === AUTH_UNAVAILABLE_CODE,\n );\n if (!hasAuthSkip) return false;\n return r.layers.every(\n (l) =>\n l.status === \"ok\" ||\n (l.layer === \"existence\" && l.code === AUTH_UNAVAILABLE_CODE),\n );\n}\n\n/** Left-pads every line of `text` by `spaces`, for indented detail blocks. */\nfunction indent(text: string, spaces: number): string {\n const pad = \" \".repeat(spaces);\n return text\n .split(\"\\n\")\n .map((l) => `${pad}${l}`)\n .join(\"\\n\");\n}\n\n/** A status glyph, coloured by severity. */\nfunction glyph(status: CheckStatus): string {\n switch (status) {\n case \"ok\":\n return pc.green(\"✓\");\n case \"warn\":\n return pc.yellow(\"⚠\");\n case \"error\":\n return pc.red(\"✗\");\n default:\n return pc.dim(\"•\");\n }\n}\n\n/**\n * Highlights actionable tokens in a detail/hint line: cyan for a \"quoted\" id or\n * `backticked` code span (delimiters stripped), bold for a SCREAMING_SNAKE env\n * var. Nesting is safe — bold's reset (22) doesn't clear the cyan (39).\n *\n * Bold runs *first*: the cyan passes wrap their match in ANSI escapes, which\n * would put a `\\x1b[36m` between the word boundary and the env-var name and stop\n * the `\\b` anchor matching — so an env var inside a code span or quotes would\n * never get bold.\n */\nfunction highlight(text: string): string {\n return text\n .replace(/\\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\\b/g, (id) => pc.bold(id))\n .replace(/`([^`]+)`/g, (_, code) => pc.cyan(code))\n .replace(/\"([^\"]+)\"/g, (_, id) => pc.cyan(id));\n}\n\n/** A sub-line beneath a row (detail or hint), consistently indented. */\nfunction subLine(text: string): void {\n console.log(`${SUB_INDENT}${text}`);\n}\n\n/**\n * Prints the expanded body of a non-ok row: each indented `detail` line, then\n * any `Hint:` set off by a blank line. Callers own the surrounding blank lines.\n */\nfunction printRowBody(\n details: Array<string | undefined>,\n hints: Array<string | undefined>,\n): void {\n for (const d of details) if (d) subLine(highlight(d));\n const realHints = hints.filter((h): h is string => Boolean(h));\n if (realHints.length > 0) {\n console.log(\"\");\n for (const h of realHints) subLine(`${pc.dim(\"Hint:\")} ${highlight(h)}`);\n }\n}\n\n/** Shows only the non-zero categories, coloured by severity. */\nfunction summaryLine(counts: {\n ok: number;\n warn: number;\n error: number;\n skipped: number;\n}): string {\n const parts: string[] = [];\n if (counts.error)\n parts.push(pc.red(`${counts.error} error${counts.error > 1 ? \"s\" : \"\"}`));\n if (counts.warn)\n parts.push(\n pc.yellow(`${counts.warn} warning${counts.warn > 1 ? \"s\" : \"\"}`),\n );\n if (counts.ok) parts.push(pc.green(`${counts.ok} ok`));\n if (counts.skipped) parts.push(pc.dim(`${counts.skipped} skipped`));\n return parts.length > 0 ? parts.join(pc.dim(\", \")) : \"nothing to check\";\n}\n\n/** Prints the leading `Auth` check row (always first). On failure, appends the\n * workspace and profile in play, a hint, and — with `--detail` — the raw SDK\n * error. The host matters as much as the profile: `DATABRICKS_HOST` overrides a\n * profile's host, so the two can come from different places. */\nfunction printAuthRow(report: DoctorReport, detail: boolean): void {\n const { auth } = report;\n console.log(` ${glyph(auth.status)} Auth — ${auth.detail ?? auth.status}`);\n // A warn carries a config problem (e.g. a host/profile split) whose hint needs\n // the same host/profile context as an outright failure.\n if (auth.status !== \"error\" && auth.status !== \"warn\") return;\n\n if (auth.host) {\n subLine(`${pc.dim(\"host:\")} ${pc.cyan(auth.host)}`);\n }\n if (auth.profile) {\n subLine(`${pc.dim(\"profile:\")} ${pc.cyan(auth.profile)}`);\n }\n if (auth.hint) {\n console.log(\"\");\n subLine(`${pc.dim(\"Hint:\")} ${highlight(auth.hint)}`);\n }\n if (detail && auth.raw) {\n console.log(\"\");\n subLine(pc.dim(\"Details:\"));\n console.log(pc.dim(indent(auth.raw, SUB_INDENT.length + 2)));\n }\n}\n\n/**\n * Prints one resource row: a tick and the name on the happy path, actionable\n * layer details beneath otherwise. Bundle-managed resources are noted as\n * deploy-created, not probed.\n */\nfunction printResourceRow(r: ResourceCheckResult): void {\n const { target } = r;\n const bundleManaged = target.origin === \"bundle-managed\";\n console.log(\n bundleManaged\n ? ` ${pc.dim(\"⧗\")} ${target.alias} ${pc.dim(\"will be created on deploy\")}`\n : ` ${glyph(r.status)} ${target.alias}`,\n );\n // Never swallow a finding: a bundle-managed row used to drop its layers, so an\n // error counted in the summary had no visible cause. `BUNDLE_MANAGED` is the\n // expected skip and would just restate the row, so it alone stays quiet.\n const problems = r.layers.filter(\n (l) => l.status !== \"ok\" && l.code !== BUNDLE_MANAGED_CODE,\n );\n if (problems.length > 0) {\n printRowBody(\n problems.map((l) => l.detail),\n problems.map((l) => l.hint),\n );\n }\n}\n\n/** Prints a wiring finding with the same shape as a resource row:\n * `glyph label` on the first line, then the indented detail and hint. */\nfunction printWiringRow(f: WiringFinding): void {\n console.log(` ${glyph(f.status)} ${highlight(f.label)}`);\n printRowBody([f.detail], [f.hint]);\n}\n\n/** A unified printable row. `expanded` rows get a blank line on either side;\n * plain `ok` rows pack together. */\ninterface Row {\n status: CheckStatus;\n expanded: boolean;\n print: () => void;\n}\n\nexport function printReport(report: DoctorReport, detail = false): void {\n const { resources, wiring, setup, summary, auth } = report;\n\n // One flat, severity-sorted list. Auth leads and never sorts.\n const rows: Row[] = [];\n rows.push({\n status: auth.status,\n expanded: auth.status === \"error\" || auth.status === \"warn\",\n print: () => printAuthRow(report, detail),\n });\n\n // When auth failed, resources whose only finding is the auth-skip carry no\n // new information, so collapse them into a single line.\n const authSkipped: ResourceCheckResult[] = [];\n const checked: Row[] = [];\n for (const r of resources) {\n if (isOnlyAuthSkipped(r)) {\n authSkipped.push(r);\n continue;\n }\n checked.push({\n status: r.status,\n // A bundle-managed row is a one-liner unless it carries a real finding, in\n // which case it needs the spacing every expanded row gets.\n expanded:\n r.status !== \"ok\" &&\n r.layers.some(\n (l) => l.status !== \"ok\" && l.code !== BUNDLE_MANAGED_CODE,\n ),\n print: () => printResourceRow(r),\n });\n }\n if (authSkipped.length > 0) {\n checked.push({\n status: \"skipped\",\n expanded: false,\n print: () =>\n console.log(\n ` ${pc.dim(\"•\")} ${pc.dim(\n `${authSkipped.length} resource${authSkipped.length > 1 ? \"s\" : \"\"} not checked (auth failed)`,\n )}`,\n ),\n });\n }\n for (const f of wiring) {\n checked.push({\n status: f.status,\n expanded: true,\n print: () => printWiringRow(f),\n });\n }\n // Setup notices share the wiring row shape (same `label`/`detail`/`hint`).\n for (const f of setup) {\n checked.push({\n status: f.status,\n expanded: true,\n print: () => printWiringRow(f),\n });\n }\n\n // Most severe first (descending severity); stable within a status.\n checked.sort((a, b) => STATUS_SEVERITY[b.status] - STATUS_SEVERITY[a.status]);\n rows.push(...checked);\n\n // A blank line sets any expanded row apart from its neighbours.\n console.log(\"\");\n rows.forEach((row, i) => {\n const prev = rows[i - 1];\n if (i > 0 && (row.expanded || prev.expanded)) console.log(\"\");\n row.print();\n });\n\n // `summary` already counts auth + wiring (built authoritatively in run.ts),\n // so render it directly — no re-folding, and it matches what --json emits.\n console.log(\"\");\n console.log(summaryLine(summary));\n // Point at --detail when there's more to show and it wasn't requested.\n if (!detail && auth.raw) {\n console.log(pc.dim(\"\\nRun with --detail for full error output.\"));\n }\n console.log(\"\");\n}\n\n/**\n * Emits the report as JSON. The raw SDK error (`auth.raw`) can carry sensitive\n * detail, so — matching the human report's `--detail` gate — it's stripped\n * unless `detail` is set. `--json --detail` opts back into the full error.\n */\nexport function printReportJson(report: DoctorReport, detail = false): void {\n const emitted =\n detail || report.auth.raw === undefined\n ? report\n : { ...report, auth: { ...report.auth, raw: undefined } };\n console.log(JSON.stringify(emitted, null, 2));\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"resolve-targets.js","names":[],"sources":["../../../../src/cli/commands/doctor/resolve-targets.ts"],"sourcesContent":["/**\n * Reads the app's resolved template manifest (`appkit.plugins.json`) and\n * flattens each plugin's declared resources into the flat {@link ResourceTarget}\n * shape the checks consume. Offline and SDK-free — parses the same file\n * `appkit plugin list` reads.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { computeOriginFromField } from \"../../../schemas/manifest\";\nimport type { ResourceTarget } from \"./types\";\nimport { errorMessage } from \"./utils\";\n\nexport const DEFAULT_MANIFEST_FILE = \"appkit.plugins.json\";\n\ninterface ManifestField {\n env?: string;\n /** Static default value baked into the manifest. */\n value?: string;\n origin?: \"user\" | \"platform\" | \"static\" | \"cli\";\n /** Resolver key (cli origin), e.g. \"postgres:host\". */\n resolve?: string;\n /** Only generated into the local .env; the platform injects it at deploy. */\n localOnly?: boolean;\n}\n\ninterface ManifestResource {\n type: string;\n resourceKey: string;\n alias?: string;\n permission: string;\n fields?: Record<string, ManifestField>;\n}\n\ninterface ManifestPlugin {\n resources?: {\n required?: ManifestResource[];\n optional?: ManifestResource[];\n };\n /**\n * True when the plugin is actually used by the app (imported and passed to\n * `createApp`). `plugin sync` discovers *every* plugin shipped by installed\n * packages but only marks the used ones; doctor checks only those, so an\n * unused built-in doesn't produce phantom \"missing env var\" errors.\n */\n requiredByTemplate?: boolean;\n}\n\ninterface TemplateManifest {\n plugins?: Record<string, ManifestPlugin>;\n}\n\n/** A field's env var is the developer's to supply only when its effective\n * origin is `\"user\"` — a stamped `origin`, else derived from the field shape. */\nfunction isUserSuppliedEnv(field: ManifestField): boolean {\n const origin = field.origin ?? computeOriginFromField(field);\n return origin === \"user\";\n}\n\n/** Env vars the config layer should presence-check (i.e. user-supplied ones). */\nfunction envVarsOf(resource: ManifestResource): string[] {\n const fields = resource.fields ?? {};\n const envs: string[] = [];\n for (const field of Object.values(fields)) {\n if (field?.env && isUserSuppliedEnv(field)) envs.push(field.env);\n }\n return envs;\n}\n\n/** Resolves each field's value keyed by manifest field name, preferring the\n * environment over a static `value` default. Unset/empty fields are omitted. */\nfunction fieldValuesOf(resource: ManifestResource): Record<string, string> {\n const fields = resource.fields ?? {};\n const values: Record<string, string> = {};\n for (const [fieldName, field] of Object.entries(fields)) {\n if (!field) continue;\n const envValue = field.env ? process.env[field.env] : undefined;\n const resolved =\n envValue !== undefined && envValue.trim().length > 0\n ? envValue\n : field.value;\n if (resolved !== undefined && resolved.trim().length > 0) {\n values[fieldName] = resolved;\n }\n }\n return values;\n}\n\nfunction toTarget(\n plugin: string,\n resource: ManifestResource,\n required: boolean,\n): ResourceTarget {\n return {\n type: resource.type,\n resourceKey: resource.resourceKey,\n alias: resource.alias ?? resource.resourceKey,\n plugin,\n requiredPermission: resource.permission,\n required,\n envVars: envVarsOf(resource),\n fieldValues: fieldValuesOf(resource),\n };\n}\n\n/** @throws if the file cannot be read or parsed. */\nexport function targetsFromManifestFile(\n manifestPath: string,\n): ResourceTarget[] {\n let raw: string;\n try {\n raw = fs.readFileSync(manifestPath, \"utf-8\");\n } catch (err) {\n throw new Error(\n `Failed to read manifest file ${manifestPath}: ${errorMessage(err)}`,\n );\n }\n\n let data: TemplateManifest;\n try {\n data = JSON.parse(raw) as TemplateManifest;\n } catch (err) {\n throw new Error(\n `Failed to parse manifest file ${manifestPath}: ${errorMessage(err)}`,\n );\n }\n\n // `plugin sync` catalogues every plugin the installed packages ship, marking\n // only those wired into `createApp` with `requiredByTemplate`. Check exactly\n // those, else doctor reports phantom \"missing env var\" errors for unimported\n // plugins.\n // KNOWN LIMITATION: sync strips `requiredByTemplate` for non-GA plugins, so a\n // used beta/experimental plugin is not checked here yet. See the README.\n const selected = Object.entries(data.plugins ?? {}).filter(\n ([, plugin]) => plugin.requiredByTemplate === true,\n );\n\n const targets: ResourceTarget[] = [];\n for (const [pluginName, plugin] of selected) {\n const resources = plugin.resources ?? {};\n for (const resource of resources.required ?? []) {\n targets.push(toTarget(pluginName, resource, true));\n }\n for (const resource of resources.optional ?? []) {\n targets.push(toTarget(pluginName, resource, false));\n }\n }\n return targets;\n}\n\n/** Returns an empty list if the manifest is absent — an app may legitimately\n * declare no resources. */\nexport function resolveTargetsFromCwd(\n cwd: string = process.cwd(),\n manifestFile: string = DEFAULT_MANIFEST_FILE,\n): ResourceTarget[] {\n const manifestPath = path.resolve(cwd, manifestFile);\n if (!fs.existsSync(manifestPath)) {\n return [];\n }\n return targetsFromManifestFile(manifestPath);\n}\n"],"mappings":";;;;;;;;;;;;AAaA,MAAa,wBAAwB;;;AAyCrC,SAAS,kBAAkB,OAA+B;AAExD,SADe,MAAM,UAAU,uBAAuB,MAAM,MAC1C;;;AAIpB,SAAS,UAAU,UAAsC;CACvD,MAAM,SAAS,SAAS,UAAU,EAAE;CACpC,MAAM,OAAiB,EAAE;AACzB,MAAK,MAAM,SAAS,OAAO,OAAO,OAAO,CACvC,KAAI,OAAO,OAAO,kBAAkB,MAAM,CAAE,MAAK,KAAK,MAAM,IAAI;AAElE,QAAO;;;;AAKT,SAAS,cAAc,UAAoD;CACzE,MAAM,SAAS,SAAS,UAAU,EAAE;CACpC,MAAM,SAAiC,EAAE;AACzC,MAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,OAAO,EAAE;AACvD,MAAI,CAAC,MAAO;EACZ,MAAM,WAAW,MAAM,MAAM,QAAQ,IAAI,MAAM,OAAO;EACtD,MAAM,WACJ,aAAa,UAAa,SAAS,MAAM,CAAC,SAAS,IAC/C,WACA,MAAM;AACZ,MAAI,aAAa,UAAa,SAAS,MAAM,CAAC,SAAS,EACrD,QAAO,aAAa;;AAGxB,QAAO;;AAGT,SAAS,SACP,QACA,UACA,UACgB;AAChB,QAAO;EACL,MAAM,SAAS;EACf,aAAa,SAAS;EACtB,OAAO,SAAS,SAAS,SAAS;EAClC;EACA,oBAAoB,SAAS;EAC7B;EACA,SAAS,UAAU,SAAS;EAC5B,aAAa,cAAc,SAAS;EACrC;;;AAIH,SAAgB,wBACd,cACkB;CAClB,IAAI;AACJ,KAAI;AACF,QAAM,GAAG,aAAa,cAAc,QAAQ;UACrC,KAAK;AACZ,QAAM,IAAI,MACR,gCAAgC,aAAa,IAAI,aAAa,IAAI,GACnE;;CAGH,IAAI;AACJ,KAAI;AACF,SAAO,KAAK,MAAM,IAAI;UACf,KAAK;AACZ,QAAM,IAAI,MACR,iCAAiC,aAAa,IAAI,aAAa,IAAI,GACpE;;CASH,MAAM,WAAW,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC,CAAC,QACjD,GAAG,YAAY,OAAO,uBAAuB,KAC/C;CAED,MAAM,UAA4B,EAAE;AACpC,MAAK,MAAM,CAAC,YAAY,WAAW,UAAU;EAC3C,MAAM,YAAY,OAAO,aAAa,EAAE;AACxC,OAAK,MAAM,YAAY,UAAU,YAAY,EAAE,CAC7C,SAAQ,KAAK,SAAS,YAAY,UAAU,KAAK,CAAC;AAEpD,OAAK,MAAM,YAAY,UAAU,YAAY,EAAE,CAC7C,SAAQ,KAAK,SAAS,YAAY,UAAU,MAAM,CAAC;;AAGvD,QAAO;;;;AAKT,SAAgB,sBACd,MAAc,QAAQ,KAAK,EAC3B,eAAuB,uBACL;CAClB,MAAM,eAAe,KAAK,QAAQ,KAAK,aAAa;AACpD,KAAI,CAAC,GAAG,WAAW,aAAa,CAC9B,QAAO,EAAE;AAEX,QAAO,wBAAwB,aAAa"}
1
+ {"version":3,"file":"resolve-targets.js","names":[],"sources":["../../../../src/cli/commands/doctor/resolve-targets.ts"],"sourcesContent":["/**\n * Reads the app's resolved template manifest (`appkit.plugins.json`) and\n * flattens each plugin's declared resources into the flat {@link ResourceTarget}\n * shape the checks consume. Offline and SDK-free — parses the same file\n * `appkit plugin list` reads.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { computeOriginFromField } from \"../../../schemas/manifest\";\nimport type { ResourceTarget } from \"./types\";\nimport { errorMessage } from \"./utils\";\n\nexport const DEFAULT_MANIFEST_FILE = \"appkit.plugins.json\";\n\ninterface ManifestField {\n env?: string;\n /** Static default value baked into the manifest. */\n value?: string;\n origin?: \"user\" | \"platform\" | \"static\" | \"cli\";\n /** Resolver key (cli origin), e.g. \"postgres:host\". */\n resolve?: string;\n /** Only generated into the local .env; the platform injects it at deploy. */\n localOnly?: boolean;\n}\n\ninterface ManifestResource {\n type: string;\n resourceKey: string;\n alias?: string;\n permission: string;\n fields?: Record<string, ManifestField>;\n}\n\ninterface ManifestPlugin {\n resources?: {\n required?: ManifestResource[];\n optional?: ManifestResource[];\n };\n /**\n * True when the plugin is actually used by the app (imported and passed to\n * `createApp`). `plugin sync` discovers *every* plugin shipped by installed\n * packages but only marks the used ones; doctor checks only those, so an\n * unused built-in doesn't produce phantom \"missing env var\" errors.\n */\n requiredByTemplate?: boolean;\n}\n\ninterface TemplateManifest {\n plugins?: Record<string, ManifestPlugin>;\n}\n\n/** A field's env var is the developer's to supply only when its effective\n * origin is `\"user\"` — a stamped `origin`, else derived from the field shape. */\nfunction isUserSuppliedEnv(field: ManifestField): boolean {\n const origin = field.origin ?? computeOriginFromField(field);\n return origin === \"user\";\n}\n\n/** Env vars the config layer should presence-check (i.e. user-supplied ones). */\nfunction envVarsOf(resource: ManifestResource): string[] {\n const fields = resource.fields ?? {};\n const envs: string[] = [];\n for (const field of Object.values(fields)) {\n if (field?.env && isUserSuppliedEnv(field)) envs.push(field.env);\n }\n return envs;\n}\n\n/** Resolves each field's value keyed by manifest field name, preferring the\n * environment over a static `value` default. Unset/empty fields are omitted. */\nfunction fieldValuesOf(resource: ManifestResource): Record<string, string> {\n const fields = resource.fields ?? {};\n const values: Record<string, string> = {};\n for (const [fieldName, field] of Object.entries(fields)) {\n if (!field) continue;\n const envValue = field.env ? process.env[field.env] : undefined;\n const resolved =\n envValue !== undefined && envValue.trim().length > 0\n ? envValue\n : field.value;\n if (resolved !== undefined && resolved.trim().length > 0) {\n values[fieldName] = resolved;\n }\n }\n return values;\n}\n\nfunction toTarget(\n plugin: string,\n resource: ManifestResource,\n required: boolean,\n): ResourceTarget {\n return {\n type: resource.type,\n resourceKey: resource.resourceKey,\n alias: resource.alias ?? resource.resourceKey,\n plugin,\n requiredPermission: resource.permission,\n required,\n envVars: envVarsOf(resource),\n fieldValues: fieldValuesOf(resource),\n };\n}\n\n/** @throws if the file cannot be read or parsed. */\nexport function targetsFromManifestFile(\n manifestPath: string,\n): ResourceTarget[] {\n let raw: string;\n try {\n raw = fs.readFileSync(manifestPath, \"utf-8\");\n } catch (err) {\n throw new Error(\n `Failed to read manifest file ${manifestPath}: ${errorMessage(err)}`,\n );\n }\n\n let data: TemplateManifest;\n try {\n data = JSON.parse(raw) as TemplateManifest;\n } catch (err) {\n throw new Error(\n `Failed to parse manifest file ${manifestPath}: ${errorMessage(err)}`,\n );\n }\n\n // `plugin sync` catalogues every plugin the installed packages ship, marking\n // only those wired into `createApp` with `requiredByTemplate`. Check exactly\n // those, else doctor reports phantom \"missing env var\" errors for unimported\n // plugins.\n // KNOWN LIMITATION: sync strips `requiredByTemplate` for non-GA plugins, so a\n // used beta/experimental plugin is not checked here yet. See the README.\n const selected = Object.entries(data.plugins ?? {}).filter(\n ([, plugin]) => plugin.requiredByTemplate === true,\n );\n\n const targets: ResourceTarget[] = [];\n for (const [pluginName, plugin] of selected) {\n const resources = plugin.resources ?? {};\n for (const resource of resources.required ?? []) {\n targets.push(toTarget(pluginName, resource, true));\n }\n for (const resource of resources.optional ?? []) {\n targets.push(toTarget(pluginName, resource, false));\n }\n }\n return targets;\n}\n\n/** Returns an empty list if the manifest is absent — an app may legitimately\n * declare no resources. */\nexport function resolveTargetsFromCwd(\n cwd: string = process.cwd(),\n manifestFile: string = DEFAULT_MANIFEST_FILE,\n): ResourceTarget[] {\n const manifestPath = path.resolve(cwd, manifestFile);\n if (!fs.existsSync(manifestPath)) {\n return [];\n }\n return targetsFromManifestFile(manifestPath);\n}\n"],"mappings":";;;;;;;;;;;;AAcA,MAAa,wBAAwB;;;AAyCrC,SAAS,kBAAkB,OAA+B;AAExD,SADe,MAAM,UAAU,uBAAuB,MAAM,MAC1C;;;AAIpB,SAAS,UAAU,UAAsC;CACvD,MAAM,SAAS,SAAS,UAAU,EAAE;CACpC,MAAM,OAAiB,EAAE;AACzB,MAAK,MAAM,SAAS,OAAO,OAAO,OAAO,CACvC,KAAI,OAAO,OAAO,kBAAkB,MAAM,CAAE,MAAK,KAAK,MAAM,IAAI;AAElE,QAAO;;;;AAKT,SAAS,cAAc,UAAoD;CACzE,MAAM,SAAS,SAAS,UAAU,EAAE;CACpC,MAAM,SAAiC,EAAE;AACzC,MAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,OAAO,EAAE;AACvD,MAAI,CAAC,MAAO;EACZ,MAAM,WAAW,MAAM,MAAM,QAAQ,IAAI,MAAM,OAAO;EACtD,MAAM,WACJ,aAAa,UAAa,SAAS,MAAM,CAAC,SAAS,IAC/C,WACA,MAAM;AACZ,MAAI,aAAa,UAAa,SAAS,MAAM,CAAC,SAAS,EACrD,QAAO,aAAa;;AAGxB,QAAO;;AAGT,SAAS,SACP,QACA,UACA,UACgB;AAChB,QAAO;EACL,MAAM,SAAS;EACf,aAAa,SAAS;EACtB,OAAO,SAAS,SAAS,SAAS;EAClC;EACA,oBAAoB,SAAS;EAC7B;EACA,SAAS,UAAU,SAAS;EAC5B,aAAa,cAAc,SAAS;EACrC;;;AAIH,SAAgB,wBACd,cACkB;CAClB,IAAI;AACJ,KAAI;AACF,QAAM,GAAG,aAAa,cAAc,QAAQ;UACrC,KAAK;AACZ,QAAM,IAAI,MACR,gCAAgC,aAAa,IAAI,aAAa,IAAI,GACnE;;CAGH,IAAI;AACJ,KAAI;AACF,SAAO,KAAK,MAAM,IAAI;UACf,KAAK;AACZ,QAAM,IAAI,MACR,iCAAiC,aAAa,IAAI,aAAa,IAAI,GACpE;;CASH,MAAM,WAAW,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC,CAAC,QACjD,GAAG,YAAY,OAAO,uBAAuB,KAC/C;CAED,MAAM,UAA4B,EAAE;AACpC,MAAK,MAAM,CAAC,YAAY,WAAW,UAAU;EAC3C,MAAM,YAAY,OAAO,aAAa,EAAE;AACxC,OAAK,MAAM,YAAY,UAAU,YAAY,EAAE,CAC7C,SAAQ,KAAK,SAAS,YAAY,UAAU,KAAK,CAAC;AAEpD,OAAK,MAAM,YAAY,UAAU,YAAY,EAAE,CAC7C,SAAQ,KAAK,SAAS,YAAY,UAAU,MAAM,CAAC;;AAGvD,QAAO;;;;AAKT,SAAgB,sBACd,MAAc,QAAQ,KAAK,EAC3B,eAAuB,uBACL;CAClB,MAAM,eAAe,KAAK,QAAQ,KAAK,aAAa;AACpD,KAAI,CAAC,GAAG,WAAW,aAAa,CAC9B,QAAO,EAAE;AAEX,QAAO,wBAAwB,aAAa"}